Content
Squircle
Give a box Apple's continuous corners, the curve that eases into the edge instead of meeting it with a circular arc.
import { Squircle, squirclePath } from "macbetter/squircle"
const size = 220
const radius = size * 0.2237
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center", gap: 32 }}>
<figure style={{ display: "grid", justifyItems: "center", gap: 10, margin: 0 }}>
<Squircle size={size} smoothing={0} style={{ background: "var(--mb-fill-secondary)" }} />
<figcaption className="mb-text-footnote mb-label-secondary">Round</figcaption>
</figure>
<figure style={{ display: "grid", justifyItems: "center", gap: 10, margin: 0 }}>
<Squircle size={size} style={{ background: "var(--mb-fill-secondary)" }} />
<figcaption className="mb-text-footnote mb-label-secondary">Continuous</figcaption>
</figure>
<figure style={{ display: "grid", justifyItems: "center", gap: 10, margin: 0 }}>
<svg width={size} height={size} viewBox={`-1 -1 ${size + 2} ${size + 2}`} role="img" aria-label="Both outlines, overlaid">
<rect width={size} height={size} rx={radius} fill="none" stroke="var(--mb-label-secondary)" strokeWidth={1.5} strokeDasharray="4 3" />
<path d={squirclePath(size, size, radius)} fill="none" stroke="var(--mb-accent)" strokeWidth={1.5} />
</svg>
<figcaption className="mb-text-footnote mb-label-secondary">Overlaid: round dashed, continuous solid</figcaption>
</figure>
</div>
)
}When to use it
Apple draws almost every rounded rectangle with a continuous corner: app icons, the Dock's tiles, Settings icons, widgets, cards, windows, sheets, alerts and menus. A circular arc meets the straight edge abruptly; a continuous corner starts curving earlier and more gently, so the shape reads as one smooth object. Next to the real thing, a plain border-radius looks slightly stamped out.
macbetter's components already use it wherever Apple does, so you only need Squircle for your own surfaces: an app icon, a tile, a widget, a thumbnail.
Don't use it on anything fully rounded. A capsule, a circle, a switch thumb or a badge is round in Apple's designs too, and a continuous corner on a full radius turns a capsule into a lozenge and a circle into a rounded square.
Usage
import { Squircle } from "macbetter/squircle"Squircle is a div with continuous corners and overflow: hidden. With no radius it has an app icon's proportion, 22.37% of its side.
import { Squircle } from "macbetter/squircle"
import { CalendarDaysIcon, CompassIcon, ImageIcon, MailIcon, MusicIcon } from "lucide-react"
const apps = [
{ name: "Mail", icon: MailIcon, from: "var(--mb-cyan)", to: "var(--mb-blue)" },
{ name: "Safari", icon: CompassIcon, from: "var(--mb-teal)", to: "var(--mb-blue)" },
{ name: "Photos", icon: ImageIcon, from: "var(--mb-yellow)", to: "var(--mb-pink)" },
{ name: "Music", icon: MusicIcon, from: "var(--mb-pink)", to: "var(--mb-red)" },
{ name: "Calendar", icon: CalendarDaysIcon, from: "var(--mb-red)", to: "var(--mb-orange)" },
]
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", justifyContent: "center", gap: 20 }}>
{apps.map(({ name, icon: Icon, from, to }) => (
<figure key={name} style={{ display: "grid", justifyItems: "center", gap: 6, margin: 0 }}>
<Squircle
size={64}
exact
shadow
style={{ display: "grid", placeItems: "center", background: `linear-gradient(160deg, ${from}, ${to})`, color: "#fff" }}
>
<Icon aria-hidden="true" strokeWidth={1.75} style={{ width: "54%", height: "54%" }} />
</Squircle>
<figcaption className="mb-text-caption">{name}</figcaption>
</figure>
))}
</div>
)
}Radius
Pass radius as a number of pixels, any CSS length or percentage, or "icon". It's the radius you'd give border-radius for the same box: the continuous corner keeps the same apparent size and only changes how it meets the edge.
import { Squircle } from "macbetter/squircle"
import { CloudSunIcon } from "lucide-react"
export default function Example() {
return (
<Squircle
radius={22}
render={<article />}
aria-label="Weather in Cupertino"
style={{
display: "grid",
gap: 4,
width: 170,
padding: 16,
background: "linear-gradient(180deg, var(--mb-blue), color-mix(in oklch, var(--mb-blue), black 35%))",
color: "#fff",
}}
>
<span className="mb-text-headline">Cupertino</span>
<span style={{ font: "300 44px/1 var(--mb-font-display)" }}>21°</span>
<CloudSunIcon aria-hidden="true" style={{ width: 20, height: 20, marginTop: 12 }} />
<span className="mb-text-footnote">Partly cloudy</span>
<span className="mb-text-footnote">H:24° L:13°</span>
</Squircle>
)
}Use render to make it another element, such as an <article>, a <button> or a link. A demo or component that passes render must be a client module.
Smoothing
smoothing sets how far the corner eases into the edge, from 0 (a circular arc) to 1. The default, 0.77, matches iOS. Lower it for a shape closer to a plain rounded rectangle.
"use client"
import { Field, FieldLabel } from "macbetter/field"
import { Slider, SliderControl, SliderValue } from "macbetter/slider"
import { Squircle } from "macbetter/squircle"
import { Switch } from "macbetter/switch"
import { useState } from "react"
export default function Example() {
const [radius, setRadius] = useState(40)
const [smoothing, setSmoothing] = useState(0.77)
const [exact, setExact] = useState(false)
return (
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "center", gap: 32 }}>
<Squircle
size={180}
radius={radius}
smoothing={smoothing}
exact={exact}
style={{ background: "linear-gradient(160deg, var(--mb-accent), var(--mb-pink))" }}
/>
<div style={{ display: "grid", gap: 16, width: 240 }}>
<Field>
<Slider value={radius} onValueChange={(value) => setRadius(value as number)} min={0} max={90} step={1}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<FieldLabel>Radius</FieldLabel>
<SliderValue />
</div>
<SliderControl />
</Slider>
</Field>
<Field>
<Slider
value={smoothing}
onValueChange={(value) => setSmoothing(value as number)}
min={0}
max={1}
step={0.01}
format={{ style: "percent" }}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
<FieldLabel>Smoothing</FieldLabel>
<SliderValue />
</div>
<SliderControl />
</Slider>
</Field>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span id="squircle-exact">Exact outline</span>
<Switch aria-labelledby="squircle-exact" checked={exact} onCheckedChange={(next) => setExact(next)} />
</div>
</div>
</div>
)
}Exact outline
corner-shape, the CSS property behind continuous corners, isn't in every browser yet. Where it's missing, a squircle has plain rounded corners. When the shape has to be exact everywhere, such as for an app icon, pass exact with a size (or width and height). The outline is computed as an SVG path while rendering, on the server too, and applied as a clip-path, so it needs no JavaScript in the browser. Without a size, exact falls back to corner-shape.
<Squircle size={64} exact>
<img src="/icons/maps.png" alt="Maps" />
</Squircle>The same path is exported as squirclePath(width, height, radius, smoothing) for your own SVG or canvas drawing.
Shadows
A clip-path cuts off anything outside the shape, including a box-shadow and a focus ring. With exact, shadow wraps the squircle in a SquircleShadow, which draws the shadow with filter: drop-shadow() around the clipped shape. Without exact, shadow is a box-shadow, which follows corner-shape on its own. To shadow any clipped element, wrap it yourself:
import { SquircleShadow, Squircle } from "macbetter/squircle"
<SquircleShadow style={{ "--mb-squircle-shadow": "0 8px 20px rgb(0 0 0 / 0.3)" }}>
<Squircle size={120} exact />
</SquircleShadow>In the Dock
DockItem tiles and the Dock's shelf use the same corner, so an icon you pass in takes the app-icon shape without a Squircle of its own.
import { Dock, DockItem } from "macbetter/dock"
import { CompassIcon, FolderIcon, MailIcon, MessageCircleIcon } from "lucide-react"
const apps = [
{ name: "Finder", icon: FolderIcon, from: "var(--mb-cyan)", to: "var(--mb-blue)" },
{ name: "Safari", icon: CompassIcon, from: "var(--mb-teal)", to: "var(--mb-blue)" },
{ name: "Messages", icon: MessageCircleIcon, from: "var(--mb-mint)", to: "var(--mb-green)" },
{ name: "Mail", icon: MailIcon, from: "var(--mb-cyan)", to: "var(--mb-indigo)" },
]
export default function Example() {
return (
<div style={{ display: "flex", alignItems: "flex-end", height: 160 }}>
<Dock aria-label="Dock">
{apps.map(({ name, icon: Icon, from, to }) => (
<DockItem key={name} label={name} running={name === "Finder"}>
<span style={{ display: "grid", placeItems: "center", background: `linear-gradient(160deg, ${from}, ${to})` }}>
<Icon aria-hidden="true" strokeWidth={1.75} style={{ width: "54%", height: "54%" }} />
</span>
</DockItem>
))}
</Dock>
</div>
)
}In your own CSS
Every macbetter component uses one convention, and you can use it in yours. Write the plain radius first, then, where the browser supports corner-shape, the continuous shape with the radius scaled by --mb-corner-factor:
.my-tile {
border-radius: 12px;
}
@supports (corner-shape: squircle) {
.my-tile {
corner-shape: var(--mb-corner-shape);
border-radius: calc(12px * var(--mb-corner-factor));
}
}The scaling matters. A superellipse at the same border-radius spans the same corner box but cuts in far less, so it looks smaller and boxier. Measured against Apple's outline, superellipse(1.7) at 1.5 times the radius is the closest CSS match: on a 360px app icon it's about 0.2px from Apple's curve on average, against 0.6px for a plain round corner. The bare squircle keyword at the unscaled radius is several pixels off (2.7px on average on a 200px icon). --mb-corner-factor is set on every element with an mb- class; on your own elements use --mb-corner-smoothing directly.
For an element without a stylesheet, add the mb-squircle class and set --mb-squircle-radius.
Never apply it to a full radius (9999px, 50% on a square, --mb-radius-full). Components whose controls become capsules on iOS keep round corners there.
Turning it off
Set --mb-corner-shape: round in a theme, or on any element, to switch continuous corners off below it. The radius scaling falls away with it, so every corner goes back to its plain size.
[data-theme="classic"] {
--mb-corner-shape: round;
}Browser support
corner-shapewithsuperellipse(): Chrome and Edge 139 and later. Firefox has it only in Nightly.- Safari 27.3 supports the
corner-shapekeywords but not yetsuperellipse(). macbetter detects this and usessquircleat 1.8 times the radius there, its closest keyword match. If you set--mb-corner-shapeto asuperellipse()yourself, Safari 27.3 falls back to round corners at the scaled radius, so prefer a keyword in a theme. - Everywhere else, corners are plain
border-radius. exactusesclip-path: path(), which every current browser supports.
Platform behavior
The shape is the same on macOS, iPadOS and iOS, as it is on Apple's platforms. What changes is where it's used: macOS rounds buttons, fields and segmented controls as small rounded rectangles, whose corners are too small for the difference to show, so they keep plain corners. iOS draws those controls as capsules, which stay round.
Accessibility
Squircleadds no role or name. Give it the semantics of what it is throughrender(an<img>, a<button>, a link) or its children, and name it.corner-shapereshapes the background, border, outline and shadow together, so a focus ring follows the corner. Aclip-pathwould cut the ring off, so on anexactsquircle that's focusable, put the focus ring on a wrapper or use the CSS mode.- Content keeps its rectangular box: text near a corner can sit outside the visible shape. Pad the content as you would for a rounded rectangle.
API
Squircle
| Prop | Type | Default | Description |
|---|---|---|---|
radius | number | string | "icon" | "icon" | The corner radius: pixels, a CSS length or percentage, or an app icon's 22.37%. |
smoothing | number | 0.77 | How far the corner eases into the edge, 0 to 1. |
size | number | — | Width and height in pixels, for a square. |
width | number | — | Width in pixels. With height, enables exact. |
height | number | — | Height in pixels. |
exact | boolean | — | Draws the true outline as a clip-path, in every browser. Needs a size. |
shadow | boolean | — | A shadow that follows the shape; a drop-shadow wrapper in exact mode. |
render | ReactElement | function | — | Renders another element with the squircle's props. |
SquircleShadow
A div that draws filter: drop-shadow(var(--mb-squircle-shadow)) around whatever its child is clipped to. Takes render and the props of a div.
squirclePath
squirclePath(width, height, radius, smoothing = 0.77): string returns the outline as an SVG path, clockwise from the top edge. A radius of 0 gives a rectangle; the largest radius, half the shorter side, gives a capsule, or a circle in a square.
Data attributes, classes and CSS variables
| Attribute, class or variable | On | Meaning |
|---|---|---|
.mb-squircle | any element | Continuous corners at --mb-squircle-radius |
data-exact | .mb-squircle | The outline is a clip-path |
data-shadow | .mb-squircle | Has a box-shadow that follows the shape |
data-smoothing | .mb-squircle | Has its own smoothing |
--mb-squircle-radius | .mb-squircle | The nominal radius, an app icon's by default |
--mb-squircle-shadow | .mb-squircle, .mb-squircle-shadow | The shadow, a single shadow |
--mb-corner-shape | any element | The corner shape, superellipse(1.7); round turns continuous corners off |
--mb-corner-smoothing | any element | How much the radius is scaled for that shape, 1.5 |
--mb-corner-factor | mb- elements | The scaling in effect: --mb-corner-smoothing, or 1 when the shape is round |
--mb-squircle-radius-icon | any element | An app icon's radius, 22.37% |