Squircle
The corner-shape property, the superellipse parameter opsinjs ships, and why this is the safest progressive enhancement in the whole system.
Overview
border-radius draws a quarter circle. Where that arc meets the straight edge,
curvature changes instantly from zero to its full value. The surfaces are
continuous but their curvature is not, and the eye is unexpectedly good at
detecting it. The corner appears to pinch.
A superellipse solves it by easing curvature in. Formally, a superellipse is the set of points satisfying
|x/a|^n + |y/b|^n = 1where n = 2 gives an ellipse and larger n progressively squares the shape
off while keeping the transition into the straight edge smooth. The n = 2
case is the circular corner you already have. opsinjs ships superellipse(4),
which is close to the corner shape Apple's platform UI has used for years and
reads as "rounded" rather than "squared" at every rung of the ladder.
The CSS property that makes this expressible is corner-shape. It sits alongside
border-radius. The radius still says how big the corner is, and corner-shape
says what curve fills it.
How it works
The declaration. Each component that opts in carries the property in its own
class list, beside its radius, rather than the product theme setting it once on
every box that opts in by attribute. In Card the shape role is
rounded-opsin-md [corner-shape:var(--opsin-corner-shape)], and Callout and
Dialog carry the same arbitrary property on their own shape role. Three
deliberate choices sit behind that one line.
It rides on the component, not on a rule in the stylesheet. The property
travels with the component into a consumer's project, where the product theme's
stylesheet may not be present. A rule that keyed the squircle off a
data-opsin-shape attribute in app/product.css would deliver nothing to a card
copied into another codebase, whereas the inline property reaches the corner
wherever the component goes.
It is not an attribute. A data-opsin-shape attribute would be a fifth
member of a data-attribute vocabulary that is closed at four, so the theme does
not stamp one. Opting in per component, on the surfaces the system draws, keeps
the enhancement off third-party components, embedded content and anything with an
incidental radius, and it does so without growing that vocabulary. No @supports
guard is needed, because an engine that does not recognise corner-shape
discards the declaration on its own.
The shape itself is a token. --opsin-corner-shape is a custom property, so
a product that wants a rounder or squarer character changes one value rather than
every component.
The degradation. Where corner-shape is unsupported, the engine drops the
inline declaration and the element keeps its border-radius, drawn with a
circular corner. It is the same size, the same
colour, in the same place, and it works identically. Nothing is conditional on
this rendering: no layout depends on it, no interaction depends on it, no
information is carried by it. That is what makes it the safest enhancement in the
system. The worst case is a corner that is slightly less pleasing than it could
be.
| Feature | Your browser | What happens without it |
|---|---|---|
| backdrop-filter | Checking… | Translucent material rungs fall back to their opaque colour. Layout is unchanged and the contrast floor is unchanged, because the fallback is the value the floor was measured against. |
| corner-shape (squircle) | Checking… | A plain rounded rectangle at the same radius. A taste difference, never a functional one. |
| Display-P3 colour | Checking… | The sRGB ramp. The escalation only raises chroma and keeps hue and lightness identical. Every measured contrast figure therefore holds in both gamuts. |
| linear() easing | Checking… | The browser's default easing. A spring is one custom property holding one linear() value, so there is no cubic-bezier approximation behind it: the declaration that substitutes it is invalid at computed-value time and the timing function is lost. Identical duration and identical end state; the curve is what goes. |
| :has() | Checking… | The status matrix filter and a handful of docs-only affordances stop filtering and show every row. Nothing in the product theme depends on it. |
Tested live with CSS.supports() in the browser you are reading this in. Nothing here is a claim about a browser matrix somebody maintained by hand.
What this is not. It is not a clip path, not an SVG mask, not a border image.
Those were the previous ways to get a squircle on the web and all three have real
costs: clipping breaks focus rings and shadows, masks break subpixel text
rendering, and both create a compositing layer that the material ladder's
performance budget cannot afford. corner-shape has none of those costs because
the browser is drawing the border, exactly as it does for border-radius.
Using it
Opt in on surfaces, not on everything. Cards, sheets, tiles and buttons are things the system draws. Not on images, not on embeds, not on video, where a shaped corner clips content rather than shaping a container.
Do not compensate for the shape. A superellipse at a given radius reads slightly larger than a circular corner at the same radius, and the temptation is to reduce the radius when the enhancement is active. Do not: the radius would then differ between browsers, the nesting arithmetic would differ with it, and the difference is smaller than the inconsistency it buys.
Do
A result card whose shape role carries
[corner-shape:var(--opsin-corner-shape)] beside its radius, taken to the
lg rung by the layout. Where corner-shape is supported it is a squircle;
where it is not it is an ordinary rounded card at the same radius. Nobody can
tell anything is missing.
Don’t
An SVG-masked squircle. It looks right in every browser and it clips the focus ring, drops the shadow, breaks text rendering on the edges and adds a composited layer to a screen that already has three.
Do not use the shape to mean anything. Same rule as radius: it is invisible to assistive technology, invisible in unsupported browsers, and therefore cannot carry information. It is character.
Check it against the material ladder. A translucent surface with a shaped corner is where the corner geometry and the backdrop blur interact, and it is worth looking at once per surface. See Materials → The ladder.
Tokens
--opsin-corner-shape and the radius ladder it applies to are generated from
tokens/shape.json by scripts/build-tokens.mts. The superellipse exponent is a
single authored value; changing it changes the character of every surface in the
product at once.
| Token | What it controls | Used by |
|---|---|---|
| --opsin-radius-base0.875rem | The one radius the whole ladder is derived from. | accordion, alert-banner, button, callout, card, care-card, combobox, empty-state, field, menu, metric-tile, popover, questionnaire, radio-group, range-bar, reading-input, result-card, scale-input, segmented-control, select, skeleton, symptom-picker, tab-bar, textarea, timeline-entry, toast, tooltip, trend-sparkline |
| --opsin-radius-none0rem | Full-bleed media and anything that meets a screen edge. | no component |
| --opsin-radius-xs0.25rem | The floor for a nested corner. Tags inside a chip, a swatch inside a legend. | popover, range-legend, surface, symptom-picker, term, tooltip |
| --opsin-radius-sm0.4375rem | Inputs and small controls inside a card. | card, checkbox, combobox, consent-sheet, dialog, divider, field, log-sheet, menu, number-field, progress, reading-input, scroll-area, select, sheet, skeleton, slider, switch, tab-bar, tabs, toast, visually-hidden |
| --opsin-radius-md0.875rem | The default. Buttons, chips, cards. | alert-banner, button, callout, card, combobox, empty-state, menu, metric-tile, popover, questionnaire, radio-group, result-card, scale-input, segmented-control, select, skeleton, symptom-picker, tab-bar, textarea, toast, tooltip |
| --opsin-radius-lg1.3125rem | Cards on a phone, where the card is nearly the width of the screen. | accordion, card, combobox, disclaimer-note, menu, popover, result-card, scroll-area, segmented-control, select, surface, toast |
| --opsin-radius-xl1.75rem | Sheets and dialogs. Applied to the leading edge only when the surface meets a screen edge on the other side. | alert-banner, dialog, sheet |
| --opsin-radius-inner-cardmax(calc(0.875rem - 5 * var(--spacing)), var(--opsin-radius-xs)) | A box inside a card at the comfortable density (card padding p-5). | no component |
| --opsin-radius-inner-card-compactmax(calc(0.875rem - 4 * var(--spacing)), var(--opsin-radius-xs)) | A box inside a card at the compact density (card padding p-4). | no component |
| --opsin-corner-shapesuperellipse(4) | The squircle curvature. Degrades to `round` where corner-shape is unsupported. | alert-banner, callout, card, dialog, metric-tile, result-card, sheet |
| --opsin-border-hairline1px | Every boundary in the system by default. | button, surface |
| --opsin-border-emphasis2px | The boundary of a surface carrying `attention` or `urgent`, where the boundary is one of the three non-colour carriers of the status. | disclaimer-note, field, surface |
| --opsin-border-focus2px | The focus ring. Always 2px with a 2px offset, always in a colour measured against both the surface and the page behind it, and never removed. See /docs/accessibility/keyboard-and-focus. | accordion, body-map, button, card, checkbox, combobox, consent-sheet, link, menu, number-field, popover, questionnaire, radio-group, scale-input, scroll-area, segmented-control, select, slider, switch, symptom-picker, tab-bar, table, tabs, term, textarea, toast, tooltip |
| --opsin-border-focus-offset2px | Offset for the focus boundary. | accordion, body-map, button, card, checkbox, combobox, consent-sheet, link, menu, number-field, popover, questionnaire, radio-group, scale-input, scroll-area, segmented-control, select, slider, switch, symptom-picker, tab-bar, table, tabs, term, textarea, toast, tooltip |
Accessibility impact
- This enhancement is accessibility-neutral by construction. Nothing is announced, nothing changes size, nothing changes position, and no interaction depends on it. That is the standard a progressive enhancement has to meet before it is allowed into the system.
- It does not clip. Unlike the mask and clip-path techniques it replaces,
corner-shapeleaves focus rings, outlines and shadows drawn normally. A clipped focus ring is a real conformance failure and it is the single most common cost of the older approaches. - It does not affect text rendering. Masking a container can force text inside it onto a different rasterisation path and lose subpixel antialiasing. That is a small legibility cost, and it lands hardest on readers who need it most.
- It changes the hit area very slightly at the corners, in the same way and
for the same reason as
border-radius, and it is accounted for in the same target-size measurement on Density and touch.
Related
- Radius scale has the rungs this shape is applied at, and the nesting rule.
- Browser support has the tested floor and the per-feature enhancement contract this is one instance of.
- Materials → Performance budget explains why the masking alternatives are not affordable.