design token

Border Radius

One radius scale, plus a cascade that keeps nested corners concentric.

Rounded corners come from one scale in rem against the design system’s 10px root, so 1.2rem is 12px. Every rounded step multiplies --radius-scale (1 unless a corner setting changes it). none is a literal 0 and full is a fixed 9999px — big enough to turn any box into a pill or, on a square, a circle.

Scale

scale · border radius
none0 · 0px
2xs0.2rem · 2px
xs0.4rem · 4px
sm0.6rem · 6pxcontrol xs · sm
md0.8rem · 8pxcontrol md
lg1.2rem · 12pxcontrol lg
xl1.6rem · 16pxcontrol xl
2xl2.4rem · 24px
3xl3.2rem · 32px
4xl4rem · 40px
5xl4.8rem · 48px
6xl6.4rem · 64px
full9999px
  • 2xs–xs (2–4px) — tiny addons, tags, the floor of every nested corner
  • sm–md (6–8px) — controls: inputs, buttons, chips
  • lg–xl (12–16px) — cards, panels, popovers (xl is Panel’s default)
  • 2xl–6xl (24–64px) — large surfaces, hero sections, playful shapes
  • full — pills, avatars, circular icon buttons

Corner settings

data-radius="sharp" or "round" on an element scales every rounded step inside it by 0.5× or 1.5×: md is 4px sharp, 8px default, 12px round. Control radii and the concentric inner.* corners follow; none and full stay put. The Appearance block sets it on <html> and remembers the choice.

<html data-radius="round">
  …
</html>
<aside data-radius="sharp">…</aside>
<!-- or any subtree -->

default is the absence of the attribute.

Control radii

Fixed-height control rows take their corner from their size, through radii.control.<size> aliases generated from the sizing presets. Reach for these when a hand-built element must sit flush beside a control of the same size:

tokens · radii.control
tokenvaluedescription
control.xs{radii.sm}6px
control.sm{radii.sm}6px
control.md{radii.md}8px
control.lg{radii.lg}12px
control.xl{radii.xl}16px
<div className={css({ borderRadius: "control.md", height: "control.md" })} />

Nested corners

A box inset inside a rounded box needs a smaller corner, or the gap between the two pinches at the bend. The concentric corner is the outer radius minus the gap, floored at 2xs so it never goes square:

inner = max(2xs, R − P)      R = parent radius, P = parent padding

Play with it — every level’s corner circle is drawn on top. When they’re concentric they share one centre and the gap stays even all the way round. Switch to same radius to see the classic mistake.

Nested corners
Outer radius
Padding
Nesting depth · 3
content

Every corner circle shares one centre — the arcs run parallel, so the gap between them is the same all the way round the bend.

<Panel radius="3xl" padding="xs">
  <Panel radius="inner" padding="xs">
    <Panel radius="inner" padding="xs">
      <Panel radius="inner" padding="xs">
        …
      </Panel>
    </Panel>
  </Panel>
</Panel>

radius="inner" on Panel

Panel (and everything built on it — Card, Dialog, Popover, …) does this for you. Give the outer panel a radius and a padding; give each nested one radius="inner":

<Panel radius="2xl" padding="sm">
  <Panel radius="inner" padding="sm">
    <Panel radius="inner">…</Panel>
  </Panel>
</Panel>

Every panel marks itself with data-panel-nest and publishes its resolved corner and padding (--panel-out-radius, --panel-out-padding). A preset rule per nesting level computes the child’s --panel-in-radius from its immediate parent, so the cascade recurses — an inner panel inside an inner panel steps down again. It’s unrolled to 8 levels; deeper than that the corner comes out slightly too round, never broken.

The floor is radii.2xs by default. Set --panel-radius-floor on any ancestor (for example var(--radii-md)) and every concentric corner in that subtree bottoms out there instead, so deeply padded trees stay visibly rounded.

radius="inherit" — flush children

A child that sits flush against its parent’s edge — a card header or footer, no gap — needs the parent’s padding-box arc, not its outer one. radius="inherit" resolves to max(0, R − border width), so no hairline of the parent’s surface shows through the corners. Pair it with a side to round only one edge, as Card’s header does:

<Panel radius="lg" padding="none">
  <Panel radius="none" radiusTop="inherit">Header</Panel>
  <div>Body</div>
</Panel>

innerRadius — outside Panel

When the nested box isn’t a Panel, use the innerRadius utility. innerRadius: "<R>.<S>" takes any radius step and spacing step and sets border-radius to max(2xs, R − S):

// a box with lg radius and md padding
<div className={css({ borderRadius: "lg", padding: "md" })}>
  <div className={css({ innerRadius: "lg.md" })}>
    Concentric corner: max(2px, 12px − 16px) = 2px
  </div>
</div>

It is computed on the element, so it follows a data-density or data-radius scope. It only knows its two inputs, so it doesn’t recurse — for nested trees prefer Panel’s radius="inner".

Directional radius

Round one side with the side shorthands (borderTopRadius, borderStartRadius, … — Start/End follow writing direction) or one corner with the longhands (borderTopLeftRadius, …):

borderTopRadius: lg
borderBottomRadius: lg
borderStartRadius: lg
borderEndRadius: lg
TL/BR 2xl · TR/BL xs
---
import { css } from "@pindoba/styled-system/css";

// Static style objects per tile — Panda extracts each one at build time.
const tiles = [
  {
    label: "borderTopRadius: lg",
    className: css({ borderTopRadius: "lg" }),
  },
  {
    label: "borderBottomRadius: lg",
    className: css({ borderBottomRadius: "lg" }),
  },
  {
    label: "borderStartRadius: lg",
    className: css({ borderStartRadius: "lg" }),
  },
  {
    label: "borderEndRadius: lg",
    className: css({ borderEndRadius: "lg" }),
  },
  {
    label: "TL/BR 2xl · TR/BL xs",
    className: css({
      borderTopLeftRadius: "2xl",
      borderBottomRightRadius: "2xl",
      borderTopRightRadius: "xs",
      borderBottomLeftRadius: "xs",
    }),
  },
];

const gridCls = css({
  display: "grid",
  gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
  gap: "md",
  width: "full",
});

const itemCls = css({
  display: "flex",
  flexDirection: "column",
  gap: "xs",
});

const tileCls = css({
  height: "80px",
  backgroundColor: "primary.surface.peak",
  borderWidth: "1px",
  borderStyle: "solid",
  borderColor: "primary.border",
});

const labelCls = css({
  fontFamily: "mono",
  fontSize: "xs",
  color: "neutral.text.muted",
});
---

<div class={gridCls}>
  {tiles.map((tile) => (
    <div class={itemCls}>
      <div class:list={[tileCls, tile.className]} />
      <span class={labelCls}>{tile.label}</span>
    </div>
  ))}
</div>

Responsive radius

Radii take responsive objects and conditions like any other property — tighten corners on small screens, round them on hover:

Resize the window
{ base: sm, md: lg, lg: 2xl }
Hover me
md → _hover: full
---
import { css } from "@pindoba/styled-system/css";

const gridCls = css({
  display: "grid",
  gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
  gap: "md",
  width: "full",
});

const itemCls = css({
  display: "flex",
  flexDirection: "column",
  gap: "xs",
});

const tileBase = {
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  height: "96px",
  padding: "md",
  fontSize: "sm",
  textAlign: "center",
  borderWidth: "1px",
  borderStyle: "solid",
} as const;

const responsiveCls = css(tileBase, {
  color: "primary.text",
  backgroundColor: "primary.surface.peak",
  borderColor: "primary.border",
  borderRadius: { base: "sm", md: "lg", lg: "2xl" },
});

const hoverCls = css(tileBase, {
  color: "success.text",
  backgroundColor: "success.surface.peak",
  borderColor: "success.border",
  borderRadius: "md",
  pdbT: "300ms",
  _hover: { borderRadius: "full" },
  _motionReduce: { transition: "none" },
});

const labelCls = css({
  fontFamily: "mono",
  fontSize: "xs",
  color: "neutral.text.muted",
});
---

<div class={gridCls}>
  <div class={itemCls}>
    <div class={responsiveCls}>Resize the window</div>
    <span class={labelCls}>{"{ base: sm, md: lg, lg: 2xl }"}</span>
  </div>
  <div class={itemCls}>
    <div class={hoverCls}>Hover me</div>
    <span class={labelCls}>md → _hover: full</span>
  </div>
</div>