Skip to content

03 β€” FOUNDATIONS

Shape

7 tokens describe every corner in the library, and only two of them are knobs. Turn one and the controls round together; turn the other and the pictures do. The rest either follow the first or are shapes rather than sizes.

ONE VARIABLE WAS NEVER GOING TO BE ENOUGH

A button and a checkbox do not want the same corner. Give the whole system a single radius and the small controls look inflated at the size the large ones look right β€” so this is a ladder, not a number. What the ladder gets you is that the rungs move together: three fifths, four fifths and five fifths of one root, which are the ratios the design already had at 3px, 4px and 5px.

Before the root existed, rounding this library meant editing three numbers that were only ever going to move as one. Three numbers that have to agree are two chances to disagree.

THE LADDER7 TOKENS Β· 2 KNOBS
--ds-radius5px

THE ROOT

The corner of the largest control, and the root the two smaller rungs are cut from. Turn this one and every control in the system rounds with it.

--ds-radius-fieldcalc(var(--ds-radius) * 3 / 5)

CUT FROM THE ROOT

Text inputs, selects, checkboxes, and the global focus ring. Three fifths of the root β€” a field is a smaller box than a button and wears a smaller corner.

--ds-radius-navcalc(var(--ds-radius) * 4 / 5)

CUT FROM THE ROOT

Menu items and the theme toggle. Four fifths of the root: navigation sits between a field and a button.

--ds-radius-controlvar(--ds-radius)

CUT FROM THE ROOT

Buttons, keys, panels, modals, the segmented group. The root itself, and the corner most of the library wears.

--ds-radius-frame0px

ITS OWN DECISION

Pictures, video and the empty placeholder. Square as shipped, and off the ladder on purpose β€” square media is a decision this design made rather than a value it forgot.

--ds-radius-pill999px

A SHAPE, NOT A SIZE

Chips and the switch track. A pill at any height, which is why it is 999px and not a corner anybody measured.

--ds-radius-round50%

A SHAPE, NOT A SIZE

Avatars, radios, the switch knob, a status dot. Half of a circle is still a circle.

Those values are read out of the theme at build time, so this table cannot drift from it. Every one of them is also a utility class β€” rounded-control, rounded-field, rounded-frame β€” for a page laying out something the library does not have a component for.

SHAPE IS A THEME SETTING

The corners live in the theme file beside the colours and the rhythm, so a theme owns the shape of a site as much as its palette. Both rows below carry identical markup. The second one sets a single variable.

SHIPPED
OPEN
SOFTER
OPEN

THE CHIP DID NOT MOVE β€” it is a pill, and a pill is a shape rather than a size. tools/check-theme.mjs reads the computed corner off a button and a field in each row and fails if they match.

For a whole site it is one line, at the root:

/* rounder, everywhere, in one line */
:root {
  --ds-radius: 12px;
}

THE ROOT IS A ROOT, AND THAT IS NOT A DETAIL

Set --ds-radius on a subtree and nothing happens. Measured: a button inside it stays at 5px. That is not a bug in the ladder, it is how custom properties work β€” a variable is substituted where it is declared, so --ds-radius-control: var(--ds-radius) resolves against the root's value and the resolved corner is what inherits down. It is the same trap @theme inline exists to avoid, one level further down.

So a subtree that wants its own shape restates the rungs, exactly as a theme does. That is what the SOFTER box above is doing, and it is the whole of it:

.softer {
  --ds-radius: 14px;
  --ds-radius-field: calc(var(--ds-radius) * 3 / 5);
  --ds-radius-nav: calc(var(--ds-radius) * 4 / 5);
  --ds-radius-control: var(--ds-radius);
}

An element carrying data-theme gets this for free, because a theme block declares the whole ladder β€” which is the ordinary way a subtree changes anything here.

THE PICTURES ARE SEPARATE

--ds-radius-frame is not on the ladder, and that is deliberate. A media box is square here because somebody decided it should be, not because nobody got around to rounding it β€” so one knob softening every control does not soften the photographs on the way past. Set it and the frame, the placeholder and any control hanging off a frame's edge all follow, which is the reason it is a token at all: those three have to agree, and left unstated they agreed by coincidence.

/* and the other direction */
:root {
  --ds-radius: 0px;        /* every control square */
  --ds-radius-frame: 12px; /* except the pictures */
}
LEAVING THE LADDER

The fractions are a default, not a cage. A theme that wants its fields crisper than its buttons restates the rung and stops deriving it β€” the same move as overriding any other variable, and nothing further down cares.

/* a rung that wants off the ladder says so outright */
:root {
  --ds-radius: 10px;
  --ds-radius-field: 2px;  /* fields stay crisp; buttons do not */
}

TURN THEM AND WATCH

Both knobs are on the theme builder as sliders, beside the colour roles, and the whole book re-shapes as you drag. Shape is stored once per theme rather than once per ground β€” the light and dark palettes of one theme are two answers about light, and a corner is not a question about light.

A CORNER IS ALWAYS A TOKEN, INCLUDING ZERO

border-radius: 0 inside a component reads as structure and is not β€” it is the same statement as --ds-radius-frame: 0px, made somewhere a theme cannot reach. The day frames soften, only one of the two follows. check-css refuses a bare corner in the four layers outright, and the four exceptions in the library are geometry rather than taste: a child declining to have a corner while its group clips it, and a ripple that is a circle by physics.