Signature
An animated SVG signature effect that draws out text as if hand-written.
Preview supplied by Arlan Marat; live previews were recorded from Arlan's Vault when no published video file was available. Preview: platform recorded · rights cleared.
An interactive comparison of ordinary rounded corners and Apple's smoothly easing squircle geometry on a live button.
VSource LandVaultWhy it stands out
A normal rounded rectangle can reveal a small kink where its curve meets a straight edge. This component demonstrates the smoother squircle curve and lets the viewer manipulate the corner treatment on a live button.
You are given the complete source for a self-contained web visual: "Apple's corners".
What it is:
- A normal rounded corner has a tiny kink where it meets the flat edge. You feel it before you can name it. Apple fixes this with a squircle, a corner that eases in smooth. Here is the real one on a live button you can play with.
How to use this code:
- It is a React + TypeScript component tree (Next.js App Router) that renders to an HTML canvas; the entry point is the playground component.
- Drop the files into a project under the same relative paths, mount the playground component, and it runs as-is. No external assets beyond what's inline.
- To adapt it: change the resolved word/logo, the color controls, or the experiment parameters. Ask me to modify, explain, or port any part.
- Original inspiration: https://www.figma.com/blog/desperately-seeking-squircles/
The full source follows, one file per block:
### squircle/superellipse.ts
```ts
// Squircle path generator. A border-radius is one circular arc per corner, and
// the arc meets the straight edge at a hard tangent, so curvature jumps 0→max in
// one step. A squircle is the superellipse |x|ⁿ+|y|ⁿ=1, where curvature ramps in
// continuously and the corner flows into the edge with no kink. We emit it as an
// SVG path and apply it with `clip-path: path(...)`.
export interface SquircleOptions {
width: number;
height: number;
/** Corner radius in px (clamped to half the shorter side). */
radius: number;
/** Corner extent along the edge (0..1). 1 = full corner, ~0.6 is a common look. */
smoothing: number;
/** Superellipse exponent n: 2 = circle, ~5 = classic squircle, higher = boxier. */
exponent?: number;
}
const DEFAULT_EXPONENT = 5;
const n = (v: number) => v.toFixed(4);
// A three-cubic-bézier fit of one superellipse corner at the default exponent, on
// a unit box: it starts at the top edge (0,0), ends at the side edge (1,1), and is
// symmetric across the (1,0)-(0,1) diagonal. Each row is [control1, control2, end];
// segment 0 starts at (0,0), each next segment starts where the previous ended.
const CORNER: readonly [number, number][][] = [
[ [0.3, 0], [0.473, 0], [0.619, 0.039] ],
[ [0.804, 0.088], [0.912, 0.196], [0.961, 0.381] ],
[ [1, 0.527], [1, 0.7], [1, 1] ],
];
// rotate a unit-corner point 90°·k clockwise (k = 0 top-right … 3 top-left)
function rot90([x, y]: [number, number], k: number): [number, number] {
switch (((k % 4) + 4) % 4) {
case 1: return [y, -x];
case 2: return [-x, -y];
case 3: return [-y, x];
default: return [x, y];
}
}
// The three cubic segments for one corner, scaled by `radius`, placed at `start`
// (where the straight edge meets the corner) and rotated by k.
function cornerCubics(sx: number, sy: number, radius: number, k: number): string {
const put = (p: [number, number]) => {
const [rx, ry] = rot90(p, k);
return `${n(sx + rx * radius)} ${n(sy + ry * radius)}`;
};
return CORNER.map(([c1, c2, end]) => `C ${put(c1)} ${put(c2)} ${put(end)}`).join(" ");
}
// For any other exponent, sample the superellipse arc directly. In unit-corner
// coords the top-right quarter of |x|ⁿ+|y|ⁿ=1 is x = sin(θ)^(2/n),
// y = 1 - cos(θ)^(2/n) for θ ∈ [0, π/2]; 32 line segments read as smooth.
function cornerSampled(sx: number, sy: number, radius: number, k: number, exponent: number): string {
const STEPS = 32;
const e = 2 / exponent;
const pts: string[] = [];
for (let i = 1; i <= STEPS; i++) {
const t = (i / STEPS) * (Math.PI / 2);
const ux = Math.pow(Math.sin(t), e);
const uy = 1 - Math.pow(Math.cos(t), e);
const [rx, ry] = rot90([ux, uy], k);
pts.push(`L ${n(sx + rx * radius)} ${n(sy + ry * radius)}`);
}
return pts.join(" ");
}
// Closed SVG path for a squircle-cornered rectangle. `smoothing` scales how far
// the corner reaches along each edge; `exponent` sets its squareness.
export function squirclePath({ width, height, radius, smoothing, exponent = DEFAULT_EXPONENT }: SquircleOptions): string {
const budget = Math.min(width, height) / 2;
const s = Math.max(0, Math.min(1, smoothing));
const r = Math.max(0, Math.min(radius, budget)) * (0.4 + 0.6 * s);
if (r <= 0) {
return `M 0 0 L ${n(width)} 0 L ${n(width)} ${n(height)} L 0 ${n(height)} Z`;
}
// The bézier fit is exact at the default exponent; sample for anything else.
const useBezier = Math.abs(exponent - DEFAULT_EXPONENT) < 0.05;
const corner = (sx: number, sy: number, k: number) =>
useBezier ? cornerCubics(sx, sy, r, k) : cornerSampled(sx, sy, r, k, exponent);
return [
`M ${n(r)} 0`,
`L ${n(width - r)} 0`,
corner(width - r, 0, 0),
`L ${n(width)} ${n(height - r)}`,
corner(width, height - r, 3),
`L ${n(r)} ${n(height)}`,
corner(r, height, 2),
`L 0 ${n(r)}`,
corner(0, r, 1),
"Z",
].join(" ");
}
// The plain border-radius path for the same box (one arc per corner), drawn as a
// dashed reference in compare mode.
export function roundRectPath(w: number, h: number, r: number): string {
const rr = Math.max(0, Math.min(r, Math.min(w, h) / 2));
return `M ${rr} 0 L ${w - rr} 0 A ${rr} ${rr} 0 0 1 ${w} ${rr} L ${w} ${h - rr} A ${rr} ${rr} 0 0 1 ${w - rr} ${h} L ${rr} ${h} A ${rr} ${rr} 0 0 1 0 ${h - rr} L 0 ${rr} A ${rr} ${rr} 0 0 1 ${rr} 0 Z`;
}
// Pick the shape path: a plain border-radius when `plain` is set, else the
// squircle. Used so the "Compare" toggle can swap every component to a plain
// rounded rect in one place.
export function shapePath(o: SquircleOptions & { plain?: boolean }): string {
return o.plain
? roundRectPath(o.width, o.height, o.radius)
: squirclePath(o);
}
// Native one-liner (Chromium-only for now; falls back to plain border-radius).
export function nativeCornerShape(radius: number): { borderRadius: string; cornerShape: string } {
return { borderRadius: `${radius}px`, cornerShape: "squircle" };
}
```
### squircle/tokens.ts
```ts
// One source of truth for every squircle demo, so they read as a single family
// instead of six independently hand-tuned components.
//
// Two control heights (a default and a small tier), one radius formula tied to
// the element's own height (so a 36px control and a 28px control get
// proportionally identical corners, and stay proportional as the sliders move),
// and shared text/tracking/padding.
/** Default control height (button, input, tabs, dropdown). */
export const CONTROL_H = 36;
/** Small control height (toggle, stepper key). */
export const CONTROL_H_SM = 28;
/** Corner radius as a fixed fraction of the element's own height. */
export function radiusFor(height: number): number {
return Math.round(height * 0.42);
}
/** Shared label size and tracking, in px. Horizontal padding is `px-3` (12px). */
export const TEXT = 14;
export const TRACKING = -0.3;
```
### squircle/Squircle.tsx
```tsx
"use client";
// Renders content inside a squircle. The body is painted (as an SVG fill or a
// clipped gradient div) and the border is a stroke on the exact outline, so the
// border is never sliced in half the way `clip-path` on the container would do.
// The element measures its own box so the path always matches its size.
import { useId, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { squirclePath, roundRectPath } from "./superellipse";
export interface SquircleProps {
radius: number;
smoothing: number;
/** Superellipse exponent (squareness): 2=circle, 5=squircle, higher=boxier. */
exponent?: number;
children?: ReactNode;
className?: string;
style?: CSSProperties;
/** Fill color or CSS gradient for the body. */
fill?: string;
/** Border color on the outline. */
stroke?: string;
/** Vertical gradient border (top → bottom), 2+ stops; overrides `stroke`. */
strokeGradient?: string[];
strokeWidth?: number;
/** Overlay the plain border-radius outline (dashed) to show the delta. */
compare?: boolean;
/** Classes for the content overlay (padding, gap, text); centered by default. */
contentClassName?: string;
}
export function Squircle({
radius,
smoothing,
exponent,
children,
className = "",
style,
fill,
stroke,
strokeGradient,
strokeWidth = 1,
compare = false,
contentClassName = "",
}: SquircleProps) {
const ref = useRef<HTMLDivElement>(null);
const uid = useId().replace(/:/g, "");
const [size, setSize] = useState({ w: 0, h: 0 });
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
const measure = () => {
// Layout size, not getBoundingClientRect (which is scaled by any ancestor
// transform), so the path stays matched to the box under a `scale()`.
const w = el.offsetWidth;
const h = el.offsetHeight;
setSize((p) => (p.w === w && p.h === h ? p : { w, h }));
};
measure();
const ro = new ResizeObserver(measure);
ro.observe(el);
return () => ro.disconnect();
}, []);
const { w, h } = size;
// `compare` swaps the shape to a PLAIN border-radius (a single circular arc per
// corner), so toggling it shows the ordinary rounded rect vs the squircle.
const body = w && h
? compare
? roundRectPath(w, h, radius)
: squirclePath({ width: w, height: h, radius, smoothing, exponent })
: "";
// A CSS gradient can't be an SVG fill, so it's painted on a clipped div instead.
const isCssGradient = !!fill && /gradient\(/.test(fill);
return (
<div ref={ref} className={className} style={{ position: "relative", ...style }}>
{isCssGradient && w > 0 && h > 0 && (
<div
aria-hidden
style={{
position: "absolute",
inset: 0,
background: fill,
clipPath: `path("${body}")`,
WebkitClipPath: `path("${body}")`,
zIndex: 0,
}}
/>
)}
{w > 0 && h > 0 && (
<svg
aria-hidden
width={w}
height={h}
viewBox={`0 0 ${w} ${h}`}
style={{ position: "absolute", inset: 0, pointerEvents: "none", display: "block", zIndex: 0 }}
>
{fill && !isCssGradient && <path d={body} fill={fill} />}
</svg>
)}
{/* Content overlay: fills the box and centers by default. */}
{children != null && (
<div
className={`absolute inset-0 flex items-center justify-center ${contentClassName}`}
style={{ zIndex: 1 }}
>
{children}
</div>
)}
{/* Border on top of everything, clipped to the shape so only its inner half
shows (drawn at 2× so the visible half equals strokeWidth). This keeps
the hairline uniform on flats and corners and stops any body/shadow layer
from painting over it. */}
{(stroke || strokeGradient) && w > 0 && h > 0 && (
<svg
aria-hidden
width={w}
height={h}
viewBox={`0 0 ${w} ${h}`}
style={{ position: "absolute", inset: 0, pointerEvents: "none", display: "block", zIndex: 3 }}
>
<clipPath id={`sq-clip-${uid}`}>
<path d={body} />
</clipPath>
{strokeGradient && (
<linearGradient id={`sq-stroke-${uid}`} x1="0" y1="0" x2="0" y2="1">
{strokeGradient.map((c, i) => (
<stop key={i} offset={i / (strokeGradient.length - 1)} stopColor={c} />
))}
</linearGradient>
)}
<path
d={body}
fill="none"
stroke={strokeGradient ? `url(#sq-stroke-${uid})` : stroke}
strokeWidth={strokeWidth * 2}
clipPath={`url(#sq-clip-${uid})`}
/>
</svg>
)}
</div>
);
}
```
### squircle/parts.tsx
```tsx
"use client";
// Squircle demo components, all sharing one glossy neutral surface.
//
// GlossySquircle is a deep stack of ~18 very low-opacity greyscale layers, each
// clipped to the squircle so it follows the corner:
// surface: porcelain gradient · corner vignette · centre glow · fine grain ·
// brushed streaks · diagonal light sweep · middle sheen band
// light: side edge-lights · top sheen · glass rim · bottom counter-sheen ·
// light lift · grounding shadow
// edge: tri-stop bevel border · inner shadow wall · fresnel edge-brighten ·
// ambient-occlusion ring · corner glints · double outer hairline
// The button, chat bubbles and segmented control are all built on it, so they read
// as one material.
import { useId, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
import { Squircle } from "./Squircle";
import { shapePath } from "./superellipse";
import { CONTROL_H, CONTROL_H_SM, radiusFor, TEXT, TRACKING } from "./tokens";
interface Shape {
radius: number;
smoothing: number;
/** Superellipse exponent (squareness): 2=circle, 5=squircle, higher=boxier. */
exponent?: number;
compare?: boolean;
}
// The glossy neutral surface. Pass a fixed size (w/h in px) OR omit them to size to
// content — it measures its own box and clips every gloss layer to the exact
// squircle of that size. Give it the corner params + content.
function GlossySquircle({
w,
h,
radius,
smoothing,
exponent,
compare,
children,
className = "",
contentClassName,
interactive = true,
}: Shape & {
w?: number;
h?: number;
children?: ReactNode;
className?: string;
contentClassName?: string;
/** press feedback (scale + shimmer). Off for passive surfaces like AI bubbles. */
interactive?: boolean;
}) {
const auto = w === undefined || h === undefined;
const measureRef = useRef<HTMLSpanElement>(null);
const [size, setSize] = useState({ w: w ?? 0, h: h ?? 0 });
useLayoutEffect(() => {
if (!auto) return;
const el = measureRef.current;
if (!el) return;
const m = () => setSize((p) =>
p.w === el.offsetWidth && p.h === el.offsetHeight ? p : { w: el.offsetWidth, h: el.offsetHeight });
m();
const ro = new ResizeObserver(m);
ro.observe(el);
return () => ro.disconnect();
}, [auto]);
const cw = auto ? size.w : (w as number);
const ch = auto ? size.h : (h as number);
const clip = cw && ch ? `path("${shapePath({ width: cw, height: ch, radius, smoothing, exponent, plain: compare })}")` : "none";
const layer = (style: CSSProperties) => (
<span
aria-hidden
className="pointer-events-none absolute inset-0"
style={{ clipPath: clip, WebkitClipPath: clip, ...style }}
/>
);
// double outer hairline only (faint dark contact ring, near-white cut), no shadow
const HAIRLINE =
"drop-shadow(0 0 0.5px rgba(0,0,0,0.18)) drop-shadow(0 0.5px 0.5px rgba(255,255,255,0.9))";
const press = interactive
? "active:scale-[0.97] active:brightness-[1.04] transition-[transform,filter] duration-150"
: "";
const gloss = (
<>
{/* corner vignette */}
{layer({ background: "radial-gradient(120% 130% at 50% 50%, rgba(0,0,0,0) 62%, rgba(0,0,0,0.045) 100%)" })}
{/* centre glow */}
{layer({ background: "radial-gradient(70% 120% at 50% 45%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 70%)" })}
{/* fine grain */}
{layer({
opacity: 0.05,
mixBlendMode: "overlay",
backgroundImage:
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")",
})}
{/* brushed streaks */}
{layer({
opacity: 0.5,
mixBlendMode: "overlay",
backgroundImage:
"repeating-linear-gradient(90deg, rgba(255,255,255,0.06) 0px, rgba(0,0,0,0.03) 1px, rgba(255,255,255,0.06) 2px)",
})}
{/* diagonal light sweep */}
{layer({
mixBlendMode: "screen",
background: "linear-gradient(115deg, rgba(255,255,255,0) 30%, rgba(255,255,255,0.28) 46%, rgba(255,255,255,0) 60%)",
})}
{/* middle sheen band */}
{layer({
background: "linear-gradient(180deg, rgba(255,255,255,0) 34%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 66%)",
opacity: 0.45,
})}
{/* side edge-lights */}
{layer({
background: "linear-gradient(90deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 8%, rgba(255,255,255,0) 92%, rgba(255,255,255,0.5) 100%)",
opacity: 0.5,
})}
{/* top sheen */}
{layer({ background: "radial-gradient(78% 82% at 50% -34%, rgba(255,255,255,0.85) 0%, rgba(255,255,255,0.28) 42%, rgba(255,255,255,0) 72%)" })}
{/* glass rim */}
{layer({ boxShadow: "inset 0 2px 0 -1px rgba(255,255,255,0.85)" })}
{/* bottom counter-sheen */}
{layer({ mixBlendMode: "screen", background: "radial-gradient(90% 60% at 50% 118%, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 60%)" })}
{/* light lift along the bottom */}
{layer({ mixBlendMode: "screen", boxShadow: "inset 0 -15px 2px -12px rgba(255,255,255,0.25)" })}
{/* grounding shadow along the bottom */}
{layer({ mixBlendMode: "color-burn", boxShadow: "inset 0 -6px 5px -2px #d2d2d2" })}
{/* inner shadow wall */}
{layer({ boxShadow: "inset 0 0 0 0.5px rgba(0,0,0,0.06)" })}
{/* fresnel edge-brighten */}
{layer({ mixBlendMode: "screen", boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.28)" })}
{/* ambient occlusion ring */}
{layer({ boxShadow: "inset 0 0 6px 0 rgba(0,0,0,0.05)" })}
{/* corner glints */}
{layer({
mixBlendMode: "screen",
background:
"radial-gradient(6px 5px at 14% 14%, rgba(255,255,255,0.75) 0%, rgba(255,255,255,0) 100%), radial-gradient(6px 5px at 86% 14%, rgba(255,255,255,0.75) 0%, rgba(255,255,255,0) 100%)",
})}
</>
);
const squircleBg = (extraStyle: CSSProperties) => (
<Squircle
radius={radius}
smoothing={smoothing}
exponent={exponent}
compare={compare}
fill="linear-gradient(180deg, #ffffff 0%, #f4f4f5 52%, #ececed 100%)"
strokeGradient={["#d3d6db", "#b4b7bd", "#7f8288"]}
strokeWidth={1}
style={extraStyle}
>
{gloss}
</Squircle>
);
// Fixed size: the Squircle is the box; content is centred inside it.
if (!auto) {
return (
<Squircle
radius={radius}
smoothing={smoothing}
exponent={exponent}
compare={compare}
contentClassName={contentClassName}
fill="linear-gradient(180deg, #ffffff 0%, #f4f4f5 52%, #ececed 100%)"
strokeGradient={["#d3d6db", "#b4b7bd", "#7f8288"]}
strokeWidth={1}
className={`${press} ${className}`}
style={{ width: w, height: h, filter: HAIRLINE }}
>
{gloss}
{children}
</Squircle>
);
}
// Auto size: the in-flow content (with its own padding) drives the box size; the
// Squircle background is absolute and sized to the measured content.
return (
<span
className={`relative inline-flex ${press} ${className}`}
style={{ filter: HAIRLINE }}
>
{cw > 0 && ch > 0 && squircleBg({ position: "absolute", inset: 0 })}
<span ref={measureRef} className={`relative z-[1] inline-flex items-center justify-center ${contentClassName ?? ""}`}>
{children}
</span>
</span>
);
}
// Debossed neutral label — two-tone near-black ink, cut into the surface with a
// base-occlusion halo, a bottom highlight, a chiselled top-highlight and a faint
// top shadow. Reused by every demo so text reads consistently.
function InkLabel({
children,
size = TEXT,
tracking = TRACKING,
className = "",
}: {
children: ReactNode;
size?: number;
tracking?: number;
className?: string;
}) {
const ls = `${tracking}px`;
return (
<span className={`relative inline-flex items-center justify-center whitespace-nowrap ${className}`}>
<span
aria-hidden
className="pointer-events-none absolute font-medium text-black/20 blur-[2px]"
style={{ fontSize: size, letterSpacing: ls }}
>
{children}
</span>
<span
className="relative z-[2] font-medium"
style={{
fontSize: size,
letterSpacing: ls,
// a hair of horizontal padding so background-clip:text never clips a glyph edge
padding: "0 0.5px",
backgroundImage: "linear-gradient(180deg, #14151a 0%, #33353b 100%)",
WebkitBackgroundClip: "text",
backgroundClip: "text",
color: "transparent",
filter:
"drop-shadow(0 1px 1px rgba(255,255,255,0.8)) drop-shadow(0 -0.5px 0.3px rgba(255,255,255,0.5)) drop-shadow(0 -0.5px 0.5px rgba(0,0,0,0.12))",
}}
>
{children}
</span>
</span>
);
}
// ── Demo 1: the glossy button ────────────────────────────────────────────────
export function SquircleGlossyButton({ smoothing, exponent, compare }: Shape & { label?: string }) {
const h = CONTROL_H;
return (
<GlossySquircle w={132} h={h} radius={radiusFor(h)} smoothing={smoothing} exponent={exponent} compare={compare}>
<InkLabel>Get the app</InkLabel>
</GlossySquircle>
);
}
// ── Demo 2: a chat — user bubble → AI (no bubble) → user bubble ───────────────
export function SquircleChat({ smoothing, exponent, compare }: Shape) {
// radius keyed to the default control height so bubbles match the family
const br = radiusFor(CONTROL_H);
const bubble = (text: string) => (
<div className="flex justify-end">
<GlossySquircle
radius={br}
smoothing={smoothing}
exponent={exponent}
compare={compare}
interactive={false}
contentClassName="px-3 py-2"
>
<InkLabel>{text}</InkLabel>
</GlossySquircle>
</div>
);
return (
<div className="flex w-[300px] flex-col gap-2.5">
{bubble("hey, what’s a squircle?")}
{/* AI answer — no bubble, just quiet text */}
<p className="max-w-[86%] text-[14px] leading-[1.5] text-[var(--text-secondary)]">
A corner that eases into the edge with no kink, like on iOS.
</p>
{bubble("love it")}
</div>
);
}
// ── Demo 3: an iOS segmented control (glossy squircle thumb slides under active)
export function SquircleTabs({
smoothing,
exponent,
compare,
active = 0,
onChange,
}: Shape & { active?: number; onChange?: (i: number) => void }) {
const items = ["Design", "Code", "Ship"];
const W = 224;
const H = CONTROL_H;
const pad = 3;
const seg = (W - pad * 2) / items.length;
const trackR = radiusFor(H); // the track is a squircle too
const tr = radiusFor(H - pad * 2); // thumb corner keyed to the thumb's height
const trackClip = `path("${shapePath({ width: W, height: H, radius: trackR, smoothing, exponent, plain: compare })}")`;
return (
<div className="relative" style={{ width: W, height: H }}>
{/* recessed track — a squircle well the thumb sits inside */}
<span
aria-hidden
className="absolute inset-0"
style={{
clipPath: trackClip,
WebkitClipPath: trackClip,
background: "linear-gradient(180deg, #e9eaec 0%, #f3f4f5 100%)",
boxShadow: "inset 0 1px 3px rgba(0,0,0,0.12)",
}}
/>
{/* sliding glossy thumb under the active segment */}
<div
className="absolute transition-[left] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{ top: pad, left: pad + active * seg, width: seg, height: H - pad * 2 }}
>
<GlossySquircle
w={seg}
h={H - pad * 2}
radius={tr}
smoothing={smoothing}
exponent={exponent}
compare={compare}
interactive={false}
/>
</div>
{/* labels — same tracking whether active or not, so they don't shift width */}
<div className="absolute inset-0 flex items-center" style={{ padding: pad }}>
{items.map((it, i) => (
<button
key={it}
type="button"
onClick={() => onChange?.(i)}
className="relative z-[1] flex h-full flex-1 items-center justify-center font-medium transition-colors"
style={{
fontSize: TEXT,
letterSpacing: `${TRACKING}px`,
color: i === active ? "transparent" : "var(--text-secondary)",
}}
>
{i === active ? <InkLabel>{it}</InkLabel> : it}
</button>
))}
</div>
</div>
);
}
// ── Demo 4: an iOS toggle switch (glossy squircle thumb slides in a squircle track)
export function SquircleToggle({
smoothing,
exponent,
compare,
on = true,
onChange,
}: Shape & { on?: boolean; onChange?: (v: boolean) => void }) {
const H = CONTROL_H_SM;
const W = Math.round(H * 1.6);
const pad = 4;
const thumb = H - pad * 2;
const trackR = radiusFor(H);
const thumbR = radiusFor(thumb);
const trackClip = `path("${shapePath({ width: W, height: H, radius: trackR, smoothing, exponent, plain: compare })}")`;
return (
<button
type="button"
aria-pressed={on}
onClick={() => onChange?.(!on)}
className="relative active:brightness-[1.04] transition-[filter] duration-150"
style={{ width: W, height: H }}
>
{/* track — a squircle well; darker when on, recessed light when off */}
<span
aria-hidden
className="absolute inset-0 transition-[background] duration-300"
style={{
clipPath: trackClip,
WebkitClipPath: trackClip,
background: on
? "linear-gradient(180deg, #4b4d52 0%, #3a3c41 100%)"
: "linear-gradient(180deg, #e5e6e8 0%, #eef0f1 100%)",
boxShadow: on
? "inset 0 1px 2px rgba(0,0,0,0.35)"
: "inset 0 1px 2px rgba(0,0,0,0.14)",
}}
/>
{/* sliding glossy thumb */}
<div
className="absolute transition-[left] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]"
style={{ top: pad, left: on ? W - pad - thumb : pad, width: thumb, height: thumb }}
>
<GlossySquircle
w={thumb}
h={thumb}
radius={thumbR}
smoothing={smoothing}
exponent={exponent}
compare={compare}
interactive={false}
/>
</div>
</button>
);
}
// A recessed squircle surface — a light inset well with a real squircle border
// (for input fields / menus). Fixed-size (w/h in px). Content is a child that
// overlays the well; it is NOT clipped, so menu rows never get cut.
function RecessedSquircle({
w,
h,
radius,
smoothing,
exponent,
compare,
children,
contentClassName = "",
}: Shape & {
w: number;
h: number;
children?: ReactNode;
contentClassName?: string;
}) {
const uid = useId().replace(/:/g, "");
const path = shapePath({ width: w, height: h, radius, smoothing, exponent, plain: compare });
const clip = `path("${path}")`;
return (
<span className="relative inline-flex" style={{ width: w, height: h }}>
{/* the recessed well: light fill + inset shadow, clipped to the squircle */}
<span
aria-hidden
className="absolute inset-0"
style={{
clipPath: clip,
WebkitClipPath: clip,
background: "linear-gradient(180deg, #f5f6f7 0%, #ffffff 100%)",
boxShadow: "inset 0 1.5px 3px rgba(0,0,0,0.10)",
}}
/>
{/* a hairline border on the squircle outline — clipped to itself so only the
inner half shows (uniform on flats + corners) */}
<svg
aria-hidden
width={w}
height={h}
viewBox={`0 0 ${w} ${h}`}
className="pointer-events-none absolute inset-0 z-[1]"
>
<clipPath id={`rec-${uid}`}>
<path d={path} />
</clipPath>
<path d={path} fill="none" stroke="#c7cace" strokeWidth={2} clipPath={`url(#rec-${uid})`} />
</svg>
{/* content overlays the well; not clipped */}
<span className={`relative z-[2] flex w-full ${contentClassName}`}>{children}</span>
</span>
);
}
// ── Demo 5: a text input — a recessed squircle field with a placeholder ───────
export function SquircleInput({ smoothing, exponent, compare }: Shape) {
const h = CONTROL_H;
return (
<RecessedSquircle
w={200}
h={h}
radius={radiusFor(h)}
smoothing={smoothing}
exponent={exponent}
compare={compare}
contentClassName="h-full items-center gap-1.5 px-3"
>
{/* search glyph */}
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" className="shrink-0 text-[var(--text-tertiary)]">
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
<path d="M20 20l-3.5-3.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
<span className="text-[var(--text-tertiary)]" style={{ fontSize: TEXT }}>Search</span>
</RecessedSquircle>
);
}
// ── Demo 6: a stepper — − / value / + as glossy squircle keys around a readout ─
export function SquircleStepper({ smoothing, exponent, compare }: Shape) {
const [n, setN] = useState(3);
const h = CONTROL_H_SM;
const key = (label: string, onClick: () => void) => (
<GlossySquircle
w={h}
h={h}
radius={radiusFor(h)}
smoothing={smoothing}
exponent={exponent}
compare={compare}
className="cursor-pointer"
>
{/* label is non-interactive; the covering button on top takes the click */}
<span className="pointer-events-none">
<InkLabel>{label}</InkLabel>
</span>
<button type="button" onClick={onClick} className="absolute inset-0 z-[4]" aria-label={label} />
</GlossySquircle>
);
return (
<div className="flex items-center gap-2">
{key("−", () => setN((v) => Math.max(0, v - 1)))}
<span className="w-6 text-center font-medium tabular-nums text-[var(--text-primary)]" style={{ fontSize: TEXT }}>{n}</span>
{key("+", () => setN((v) => v + 1))}
</div>
);
}
// ── Demo 7: a dropdown — a glossy squircle field that opens a squircle menu ────
export function SquircleDropdown({ smoothing, exponent, compare }: Shape) {
const options = ["Squircle", "Rounded", "Circle"];
const [open, setOpen] = useState(false);
const [sel, setSel] = useState(0);
const W = 180;
const r = radiusFor(CONTROL_H);
return (
<div className="relative">
<GlossySquircle
w={W}
h={CONTROL_H}
radius={r}
smoothing={smoothing}
exponent={exponent}
compare={compare}
className="cursor-pointer"
contentClassName="justify-between px-3 pointer-events-none"
>
<InkLabel>{options[sel]}</InkLabel>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="ml-2 shrink-0 text-[var(--text-secondary)]" style={{ transform: open ? "rotate(180deg)" : undefined, transition: "transform .2s" }}>
<path d="M6 9l6 6 6-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{/* the click target, on top of the gloss + label */}
<button type="button" onClick={() => setOpen((o) => !o)} className="pointer-events-auto absolute inset-0 z-[4]" aria-haspopup="listbox" aria-expanded={open} />
</GlossySquircle>
{open && (
// opens UPWARD (bottom-full) so it isn't clipped by the preview's bottom edge
<div className="absolute bottom-full left-0 z-10 mb-2" style={{ width: W }}>
<RecessedSquircle
w={W}
h={options.length * 34 + 8}
radius={r}
smoothing={smoothing}
exponent={exponent}
compare={compare}
contentClassName="flex-col p-1"
>
{options.map((o, i) => (
<button
key={o}
type="button"
onClick={() => { setSel(i); setOpen(false); }}
className="flex h-[34px] w-full items-center rounded-lg px-3 text-left transition-colors hover:bg-black/[0.05]"
style={{ fontSize: TEXT, color: i === sel ? "var(--text-primary)" : "var(--text-secondary)", fontWeight: i === sel ? 600 : 400 }}
>
{o}
</button>
))}
</RecessedSquircle>
</div>
)}
</div>
);
}
```
### squircle/playground.tsx
```tsx
"use client";
// The squircle on a few real components — a glossy button, a chat, an iOS
// segmented control — all sharing one neutral material. Remix shuffles which one
// is shown; two sliders tune the corner; Compare / Remix sit beside the title.
import { useState } from "react";
import { PG_PREVIEW, PG_PANEL, Slider, GhostButton } from "../swirl/controls";
import { SectionLabel } from "../section-label";
import { hapticTap } from "../../lib/haptics";
import {
SquircleGlossyButton,
SquircleChat,
SquircleTabs,
SquircleToggle,
SquircleInput,
SquircleStepper,
SquircleDropdown,
} from "./parts";
const DEFAULT_SMOOTHING = 1;
const DEFAULT_EXPONENT = 5;
const RADIUS = 22;
const DEMOS = 7;
export function SquirclePlayground() {
const [smoothing, setSmoothing] = useState(DEFAULT_SMOOTHING);
const [exponent, setExponent] = useState(DEFAULT_EXPONENT);
const [compare, setCompare] = useState(false);
const [demo, setDemo] = useState(0);
const [tab, setTab] = useState(0);
const [toggleOn, setToggleOn] = useState(false);
// Remix: step to the next demo in order.
const remix = () => {
hapticTap();
setDemo((d) => (d + 1) % DEMOS);
};
const shape = { radius: RADIUS, smoothing, exponent, compare };
return (
<div className="flex min-w-0 flex-col gap-4">
<SectionLabel
action={
<div className="flex items-center gap-2">
<GhostButton onClick={() => setCompare((c) => !c)}>
{compare ? "Compare on" : "Compare"}
</GhostButton>
<GhostButton onClick={remix}>Remix</GhostButton>
</div>
}
>
Playground
</SectionLabel>
<div className="flex flex-col">
<div className={`${PG_PREVIEW} flex min-h-[260px] items-center justify-center px-6 py-12`}>
{demo === 0 && <SquircleGlossyButton {...shape} />}
{demo === 1 && <SquircleChat {...shape} />}
{demo === 2 && <SquircleTabs {...shape} active={tab} onChange={setTab} />}
{demo === 3 && <SquircleToggle {...shape} on={toggleOn} onChange={setToggleOn} />}
{demo === 4 && <SquircleInput {...shape} />}
{demo === 5 && <SquircleStepper {...shape} />}
{/* nudged down so the upward-opening menu clears the top without growing the stage */}
{demo === 6 && (
<div className="translate-y-10">
<SquircleDropdown {...shape} />
</div>
)}
</div>
<div className={PG_PANEL}>
<Slider
label="Smoothing"
value={smoothing}
min={0}
max={1}
step={0.01}
format={(v) =>
v < 0.005
? "0.00 · plain"
: Math.abs(v - 0.6) < 0.005
? "0.60 · iOS"
: v.toFixed(2)
}
onChange={setSmoothing}
/>
<Slider
label="Squareness"
value={exponent}
min={2}
max={8}
step={0.1}
format={(v) =>
Math.abs(v - 2) < 0.05
? "2.0 · circle"
: Math.abs(v - 5) < 0.05
? "5.0 · iOS"
: `n ${v.toFixed(1)}`
}
onChange={setExponent}
/>
</div>
</div>
</div>
);
}
```Discovery vocabulary
Related by governed terms
An animated SVG signature effect that draws out text as if hand-written.
More from Vault