Skip to main content

Harshal V. LADHE

Squircles, Bevels and Scoops with corner-shape

Shape corners into squircles, bevels, scoops and notches.
Published at:
Last updated:
Estimated reading time:5 min read

Introduction

For as long as CSS has had border-radius, a corner could be exactly one shape: a quarter of an ellipse. Anything else — the smooth "squircle" of an app icon, a bevelled sale tag, the scooped edge of a ticket stub — meant reaching for SVG, a clip-path polygon, or a stack of pseudo-elements. Each of those has a catch: clip-path cuts off your borders and shadows, and SVG has to be redrawn whenever the box changes size.

corner-shape fixes this at the source. border-radius keeps deciding how big each corner is; corner-shape decides what shape fills that space. Because it changes the box itself, the border, background, box-shadow and outline all follow the new shape automatically.

Basic Usage

.app-icon {
  border-radius: 30%;
  corner-shape: squircle;
}

Two things to notice:

  • border-radius is still required. It sets the size of each corner. With a radius of 0 there's no corner to shape, so corner-shape has nothing to do.
  • Everything follows the shape. Unlike clip-path, which only crops what's already drawn, the border, the background, any box-shadow (including its spread and offset) and the outline are all drawn along the new corners. That includes a gradient border built with background-clip, which takes on the shape for free.

The Six Keywords

KeywordWhat the corner looks like
roundThe ordinary elliptical curve — the initial value
squircleA smoother, "continuous" curve, like an app icon
squareNo curve at all: a sharp right angle, whatever the radius
bevelA straight diagonal cut across the corner
scoopA concave curve that bites into the box, like a ticket stub
notchA square step cut into the corner

Every keyword is a named point on one scale, the superellipse() function, which you can also use directly.

Fine Control with superellipse()

.card {
  border-radius: 2rem;
  corner-shape: superellipse(1.5);
}

superellipse() takes one number that sets how far the corner bulges outward (positive) or bites inward (negative). The keywords map onto it like this:

  • notch is superellipse(-infinity)
  • scoop is superellipse(-1)
  • bevel is superellipse(0), a straight line
  • round is superellipse(1)
  • squircle is superellipse(2)
  • square is superellipse(infinity)

So superellipse(1.5) sits between an ordinary curve and a squircle, and superellipse(-0.5) gives a gentler scoop than the keyword does.

Different Shapes per Corner

.speech-bubble {
  border-radius: 1.25rem;
  /* top-left, top-right, bottom-right, bottom-left */
  corner-shape: round round square round;
}

Like border-radius, corner-shape takes one to four values, in the same clockwise order starting at the top-left. There are longhands too — physical ones such as corner-top-left-shape, and logical ones such as corner-start-start-shape that follow the writing direction, just as logical properties do.

Combining it with an uneven border-radius opens up more shapes. A radius on the right-hand corners only, bevelled, turns a plain button into an arrow:

.next-button {
  border-radius: 0 1.25rem 1.25rem 0;
  corner-shape: bevel;
}

It Animates

corner-shape interpolates between values, so a transition morphs the corners smoothly rather than snapping:

.card {
  border-radius: 2rem;
  corner-shape: scoop;
  transition: corner-shape 0.6s ease;
}

.card:hover {
  corner-shape: squircle;
}

The browser animates the superellipse() value underneath, so even keyword-to-keyword transitions pass through every shape in between. Duration, easing and delay work exactly as they do for any other property — the CSS transitions guide covers them in depth. As with any decorative motion, wrap the transition in a prefers-reduced-motion: no-preference query (or turn it off under reduce) for readers who've asked for less of it.

Try It Live

The Explorer tab shapes one card that has a border, a shadow and an outline, so you can see every layer follow the corners. Pick a keyword, choose superellipse() to drag the value from -4 to 4, change the border-radius, and turn on Animate on hover to watch it morph. The panel underneath shows the CSS for exactly what you see.

The Gallery tab shows each keyword doing a real job — app icons, tags, ticket stubs, panels, speech bubbles and directional buttons — every one a single element with no SVG or clip-path.

corner-shape Playground

Shape one bordered, shadowed card with every corner-shape keyword or a superellipse() value, change its border-radius and morph it on hover, then browse squircles, bevels, scoops and notches doing real UI jobs.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>corner-shape Playground</title>
  </head>
  <body>
    <main class="playground">
      <header class="playground-header">
        <h1>Shaping Corners with corner-shape</h1>
        <p><code>border-radius</code> sets how big each corner is; <code>corner-shape</code> sets what shape fills that space. The border, background, shadow and outline all follow it.</p>
      </header>

      <p class="support-note" id="support-note" hidden>This browser doesn't support <code>corner-shape</code> yet, so every corner falls back to an ordinary rounded <code>border-radius</code>.</p>

      <div class="tabs" role="tablist" aria-label="Demo" id="demo-switch">
        <button class="tab tab--active" type="button" data-mode="explorer" role="tab" aria-selected="true">Explorer</button>
        <button class="tab" type="button" data-mode="gallery" role="tab" aria-selected="false">Gallery</button>
      </div>

      <div id="explorer-demo" class="tab-panel tab-panel--active">
        <section class="demo-area" aria-label="Shape preview">
          <div class="stage">
            <div class="shape-card" id="shape-card">
              <strong>Card</strong>
              <span>Hover me when "Animate on hover" is on</span>
            </div>
          </div>
        </section>

        <fieldset class="control-group">
          <legend>corner-shape</legend>
          <div class="segmented-control" role="radiogroup" aria-label="Corner shape">
            <label class="segment"><input type="radio" name="shape" value="round" checked><span>round</span></label>
            <label class="segment"><input type="radio" name="shape" value="squircle"><span>squircle</span></label>
            <label class="segment"><input type="radio" name="shape" value="square"><span>square</span></label>
            <label class="segment"><input type="radio" name="shape" value="bevel"><span>bevel</span></label>
            <label class="segment"><input type="radio" name="shape" value="scoop"><span>scoop</span></label>
            <label class="segment"><input type="radio" name="shape" value="notch"><span>notch</span></label>
            <label class="segment"><input type="radio" name="shape" value="superellipse"><span>superellipse()</span></label>
          </div>

          <div class="fields-grid">
            <div class="field" id="curvature-field">
              <label for="curvature-input">superellipse() value</label>
              <div class="range-row">
                <span class="range-edge">-4</span>
                <div class="range-track">
                  <input type="range" id="curvature-input" min="-4" max="4" step="0.1" value="1.5">
                  <output class="range-bubble" id="curvature-value" for="curvature-input">1.5</output>
                </div>
                <span class="range-edge">4</span>
              </div>
            </div>

            <div class="field">
              <label for="radius-input">border-radius</label>
              <div class="range-row">
                <span class="range-edge">0px</span>
                <div class="range-track">
                  <input type="range" id="radius-input" min="0" max="60" step="1" value="36">
                  <output class="range-bubble" id="radius-value" for="radius-input">36px</output>
                </div>
                <span class="range-edge">60px</span>
              </div>
            </div>
          </div>

          <label class="checkbox-field">
            <input type="checkbox" id="animate-toggle">
            Animate on hover (from <code>scoop</code> to the chosen shape)
          </label>
        </fieldset>

        <output class="readout" id="readout" aria-live="polite"></output>
      </div>

      <div id="gallery-demo" class="tab-panel">
        <section class="demo-area" aria-label="corner-shape keywords as real UI">
          <div class="gallery">
            <figure>
              <div class="app-icon" aria-hidden="true">Aa</div>
              <figcaption><code>squircle</code><br>App icons</figcaption>
            </figure>
            <figure>
              <div class="tag-label">Sale</div>
              <figcaption><code>bevel</code><br>Tags and labels</figcaption>
            </figure>
            <figure>
              <div class="ticket">Admit one</div>
              <figcaption><code>scoop</code><br>Ticket stubs</figcaption>
            </figure>
            <figure>
              <div class="notched-panel">Panel</div>
              <figcaption><code>notch</code><br>Sci-fi panels</figcaption>
            </figure>
            <figure>
              <div class="speech-bubble">Hello!</div>
              <figcaption><code>round round square round</code><br>Speech bubbles</figcaption>
            </figure>
            <figure>
              <div class="cut-button">Next</div>
              <figcaption><code>bevel</code>, right corners only<br>Directional buttons</figcaption>
            </figure>
          </div>
        </section>
        <p class="tip">Every shape here is one element with a <code>border-radius</code> and a <code>corner-shape</code> — no SVG, no <code>clip-path</code>, no pseudo-elements.</p>
      </div>
    </main>

    <script src="./index.js"></script>
  </body>
</html>

Read-only
Ln –, Col –HTML5.3 KBUTF-8

Starting sandbox…

No console output yet.

Browser Support and Fallbacks

corner-shape shipped in Chromium browsers (Chrome and Edge 139) in 2025. Other browsers don't support it yet at the time of writing — and that's fine, because the fallback is automatic. A browser that doesn't understand corner-shape ignores it and draws the ordinary rounded corners from border-radius, so the design degrades to something perfectly usable.

When a shape needs a different radius to look right, adjust it only where the shape will actually render:

.ticket {
  border-radius: 0.5rem;
}

@supports (corner-shape: scoop) {
  .ticket {
    border-radius: 1rem;
    corner-shape: scoop;
  }
}

Common Pitfalls

1. Forgetting the Radius

corner-shape on its own does nothing — the corner has no size until border-radius gives it one. If a shape doesn't appear, check the radius first.

2. Content Running into Cut-Outs

corner-shape reshapes the box, not its content. Concave shapes like scoop and notch cut into the padding area, so text placed near a corner can overlap the cut-out. Give those elements enough padding to keep content clear.

3. Large Radii with Concave Shapes

The bigger the radius, the deeper a scoop or notch bites. On a small element, a large radius can cut away most of the box — keep concave radii modest relative to the element's size.

Key Takeaway

border-radius sets the size of a corner; corner-shape sets its shape. Together they replace most of the SVG masks, clip-path polygons and pseudo-element tricks once needed for squircles, bevels, scoops and notches — and unlike those, the border, shadow and outline follow along, it animates, and browsers without support simply fall back to rounded corners.

Changelog

  • — Initial publication.
This tutorial is licensed under CC BY 4.0 by the author.

Share this tutorial

Spotted a mistake or something outdated in this tutorial? Suggest a correction.

  • CSS-Only Staggered Animations with sibling-index() and sibling-count()

    Stagger animations from each element's own position, without JavaScript.
    sibling-index() and sibling-count() give an element its own 1-based position and its sibling total directly inside calc() — replacing the JavaScript loop that sets a --i custom property, with fixed-step and fixed-total staggers, index-driven colours and rotations, fallbacks, and reduced-motion handling.
    Published at:
  • HTML Attributes as Real CSS Values with Typed attr()

    Read HTML attributes as CSS colours, lengths and numbers.
    Typed attr() reads an HTML attribute as a real CSS value — type(<color>), type(<integer>), a unit like % or px, or raw-string — for any property on any element, not just text in content. Covers fallbacks for missing or invalid values, patterns for layout, transitions and view transitions, safe fallbacks in browsers without support, and when a custom property is the better choice.
    Published at:
  • Styling a <select> Dropdown with CSS appearance: base-select

    Style a real select's dropdown, options and arrow.
    appearance: base-select opts a native <select> into a fully styleable mode — rich HTML in options, <selectedcontent> in the closed button, and ::picker(select), ::picker-icon and ::checkmark for the dropdown, arrow and tick — while keeping its keyboard support, accessibility and form behaviour, with a plain select as the fallback.
    Published at: