The dial was drawn five ways before one was chosen. This is the same
exercise for the thing next to it. There are already three buttons in the house that were
never named as buttons — the mark, the pill, and a boxed one drawn five ways across
four apps — and CHROME.md counts the cost: 63 button rules in
font-proofer, 50 in ReCal, 16 in Kernpare, most of them that same box with its own
padding and its own radius.
Two things get decided here, and only two. One enclosure — what a thing looks like when pressing it does something. One editable mark — what text looks like when it is yours to change. They are one decision because they are the same question asked twice, and because getting the second one wrong is how you end up with a bulleted list of unformatted links that each open a modal, with nothing on the page to say so. Every family below answers both in its own language, on the same content, so the comparison is between designs and not between demos.
A press is answered twice. :active acknowledges it
while the pointer is down — unchanged. When the press worked, the control
confirms it the way macOS blinks a menu item: the held look goes off for
80ms, comes back on for 80ms, then the control settles into its result. A
blink, not a fade; paint only, nothing moves. Every button on this page does it, each
in its own held look. Press these:
Not on a drag, not on a hold-repeat (a stepper held past 400ms confirms
nothing), not on a press that failed its own check. Reduced motion: no blink, straight to
the result. In the system it is wmConfirm(el) from src/confirm.js
and one keyframe set in src/motion.css; GESTURES.md §14.
| radius | A constant, never a function of the box. A display button takes the same corner as the one it is louder than; only the type, the stroke and the space change with size. The pill is the exception that proves it — 999px means half the height, so it is allowed only on a single-line control whose height is fixed, and anything that can wrap takes the constant instead. |
| corner | superellipse(1.2), radius on the 2^k scale. Safari has no corner-shape and degrades silently to the plain radius — which is the corner every app shipped before the law, so nothing regresses. |
| target | 44px on a coarse pointer, bought as padding, never as height. A target has to exist; it does not have to be visible, and it must not be paid for by shrinking the mark. |
| padding-x | ≥ 0.6 × the radius. On a pill the radius is half the height, so a 34px pill owes 10.2px each side. |
| ink | faint / quiet / full = cannot act / present / chosen. Hover and pressed must not share an ink, or the ladder collapses. |
| mark | Material Symbols, one weight (300). Emphasis rides GRAD, so a row of marks cannot shift by a fraction as one lights up. |
| press | The stroke lands before the fill: the stroke acknowledges the tap, the fill confirms on release. |
| confirm | A press that worked blinks its held look: off 80ms, on 80ms, then the result. Never on a drag or a repeat. |
| focus | 2px, offset 2, on the signal. Never removed, in any family. |
| label | A button is text. A mark is an addition that has to earn itself, and most labels never do: “Kern lists”, “10 Reg”, “Average pairs” and “Next specimen” name things no icon is about, and giving them one anyway is how a set ends up with marks that decorate instead of identify. Every button below is therefore text only. Two exceptions, both deliberate: a menu row, where the items are of equal weight and you are picking between them rather than reading them — so the mark is what you scan and the name is the confirmation; and the icon-only button, which is not a label with a mark but a target that is only a mark. |
A box, and one side of it. A hairline on a surface, and an editable line is the same box drawn with only its bottom edge — same radius, same inset, same color — which completes as you approach. The law is one sentence: a closed keyline means press; a bottom-only keyline means change. It holds only if no rule under a heading is ever decorative.
Screamer
Loud is not filled. The keyline thickens to 2.5px and takes full ink on both the edge and the label. It is still the same box — which is the point of the family.
Buttons
Menu
States
Editable
In situ
What it costs. Nothing separates a resting editable line from a
divider under a heading, so the family is only honest in a document that never draws a
decorative rule. In exchange it is the cheapest thing here: src/button.css is
already this box, and the editable mark is four declarations on top of it.
A ground, not an edge. No hairline anywhere in the family. An action is a filled pill; an editable is the same ground with the fill taken almost all the way out — a wash at 5% that comes up to 12% as you approach. The wash takes the constant corner rather than the pill, because a wash wraps and a pill's radius would grow with it. Highlighter instead of rule, so the mark sits behind the words and never competes with the type's own baseline furniture.
Screamer
The nearest thing to the real hero pill: signal ground, radius half the height.
Buttons
Menu
States
Editable
In situ
What it costs. Width. The padding rule is not optional on a pill,
and four icon buttons in a row become a scroll. The resting wash is the real risk: at 5%
on --surface it is almost invisible, so this family needs the editable text
to sit on --bg, and it needs testing on a screen that is not the one it was
drawn on. It also has the strongest primary in the set — one signal pill in a view
and nothing else competes.
Graduated 2026-10-08 as the house secondary, .wm-rule in src/button-sets.css — system page, The corner law, “The button”.
The type is the control. No box in any state. An action is
ink-full with a solid rule at 1px, thickening to the same two numbers the
press law uses — 1.2 under the pointer, 1.8 held. An editable is the identical rule,
dashed. Solid means it happens; dashed means it is yours. This is the one family
that can be wrong in the exact way the brief names, and the condition is absolute:
nothing else in the document may be underlined.
Screamer
No box at any size. The rule thickens with the type instead of staying 1px.
Buttons
No icon-only form. There is nothing to draw it in. Those actions borrow 01.
Menu
And this is where the family runs out. The rule scopes to the text, so in a row the leading mark, the label, and the trailing mark are three objects at the same height with nothing holding them together — an underline cannot reach across a flex gap, and the row never becomes one thing. A box does that for free, which is the argument for a box. Widening the rule to span the whole row does bind it, and then it is a divider: the one mark this family has already means "press", so it cannot also mean "these belong together".
States
Editable
In situ
What it costs. Three things. The 44px floor is bought entirely with invisible padding, so two stacked actions are 88px of line box or they overlap. Two actions side by side read as one sentence unless something separates them. And the whole family depends on a document-wide prohibition that no component can enforce — the first underlined caption someone adds takes the affordance down with it.
Hills and holes. Raised is press, recessed is type — and nobody has to be taught it. One lightness ramp does all of it: the plate is +L on the ground with the light from above, the well is −L with the light inside, and there is no hue and no second palette anywhere. Held, the plate goes into the ground rather than changing color.
Screamer
Still a plate, not a fill. The lift stays a 1px highlight and the drop goes from 2px to 6px: the hill gets taller rather than becoming a color.
Buttons
Menu
States
Editable
In situ
What it costs. The corner law already records the fault, and this
family is built on the thing that has it: an inset box-shadow follows
border-radius but not corner-shape, so every well and every
held plate paints a rounded rectangle inside a superellipse, visible at the corners. That
is not a bug to fix here — it is the engine — so choosing 04 means either
keeping the radius small enough to hide it or giving up the house corner on exactly the
controls that carry it most. It is also the only family that reads as a different decade.
Corner ticks that close on approach. The box is there; only its
corners are inked. --bt-tick is how far the ink runs along each edge, and
hover animates it to 50% — at which point the four ticks meet and the enclosure
completes. The ticks are a real border under two intersected masks, so they carry the G2
curve rather than imitating it. Editable text keeps only the left and right pair:
brackets around the words, the way a field is marked on a drawing.
Screamer
The ticks get heavier instead of surrendering. 3px, full ink, run out to 26px — the family's own device at display scale rather than a borrowed fill.
Buttons
Menu
States
Editable
In situ
What it costs. mask-composite: intersect is the
whole mechanism; where it is missing the mask collapses and the full border paints, which
is family 01 — a correct button, so the degradation is honest, but a family whose
fallback is another family is one design being maintained as two. At UI size the tick is
9px and four of them in a dense menu read as noise rather than as an edge. The transition
needs --bt-tick registered with @property, or the ticks snap.
Graduated 2026-10-08 as the house primary, .wm-menu in src/button-sets.css — system page, The corner law, “The button”.
An enclosure means chosen, not pressable. The inversion of the
five above, and the only one already running: it is what font-proofer's rail already
does, and what that is, is a menu — preview modes of equal weight, picked
between rather than read, which is the one place on this page where a mark earns its
space. A row has no
edge at rest — a mark, a label, and the full width of the column as its target
— and the ground arrives when it is the one that is on. The editable mark is not a
proposal either: it is .slider-number, the rule the dial ships. An
underline drawn on the text, not a border drawn on the box — exactly as wide as
the value, at the baseline rather than under the descender, and focus takes the digits
and the rule together.
Screamer
And here is the family's ceiling: chosen IS the loudest thing it owns.
Buttons
The primary is the giveaway: 06 spends its enclosure on chosen, so the one action that matters can only borrow the same ground. There is no louder left to go.
Menu
States
Editable
In situ
What it costs. A resting row states nothing on its own — which is the brief’s failure exactly, and the family survives it only on two conditions it cannot enforce for itself: every row carries a mark, and every row is the full width of its column. Strip either and this becomes the unformatted list. It also needs somewhere to be a menu: the row shape wants 200px and a left edge to sit against, so an action in the middle of a paragraph has to borrow another family. And because the enclosure is spent on chosen, there is nothing left to spend on primary — a destructive action here looks like every other row.
And it does not do prose. Visible in the situ card above: the
rule is drawn on the text, so on a value it is four characters wide and on a wrapped
sentence it is two full lines of underline. .slider-number was written for a
number in a rail and it is right there. The editable case in the brief is a headline,
not a value — so if 06 wins the menu, something else still has to win the
paragraph, and the two have to look like one decision.
All six draw the same six specimens, in the same order, on the same content — screamer, buttons, menu, states, editable, in situ — so a column means one thing all the way down the page and a difference between two cells is a difference between designs. Where a cell is empty the family cannot do that thing, and that is the finding — not an omission.
| family | screamer | buttons | menu | 5 states | editable | in situ | cannot do |
|---|---|---|---|---|---|---|---|
| 01 Keyline | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| 02 Lozenge | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| 03 Rule | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | an icon-only button — nothing to draw it in; and a row — the rule scopes to the text, so a leading mark, a label, and a trailing mark never become one object |
| 04 Plate | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | the house corner on its own wells — inset shadow follows border-radius, not corner-shape |
| 05 Bracket | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — (its cost is legibility: 9px ticks read as noise in a dense menu, not as an edge) |
| 06 Menu | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | a primary louder than chosen, and an editable that is prose rather than a value |
The screamer is not the UI button made bigger, and that is why it
earns a row. The menu runs at opsz 10; Cal Sans's axis is 8 – 14 – 45
and a display line belongs at the top of it. Scaling a 13px button to 34px and leaving the
optical size alone prints the small drawing large — thicker stems, looser
fit — which is mis-drawn rather than merely big. Every screamer here runs
opsz 45 and GEOM 50, which is what the real hero pill runs.
Weight is not part of “display”, though. The hero pill
is also wght 700, and copying that to all six was a second emphasis stacked on
whatever the enclosure was already doing — the rule icon.css states for
the mark: once one thing carries the emphasis, a heavier stroke on top is two emphases
stacked. A 2.5px keyline, a 0.1em rule, 3px ticks, and a raised plate are each loud by
themselves, so their type stays at 400, where Cal Sans is drawn. 02 keeps 700,
and for a legibility reason rather than a volume one: it is the only screamer that inverts
— dark ink on a signal field — and weight there is holding the counters open
against a flat color.
It reads tight on top because the bottom is carrying space nothing
occupies. The face is left alone — no axis is touched. With
line-height: 1 the baseline sits 0.8275em down, the cap top 0.1075em down, and
the descender falls 0.0650em past the bottom of the line box; the hero pill then
adds 0.38em of padding below against 0.22em above, which leaves 0.315em of clear air under
the descender and a top gap that is mostly empty line box. text-box-trim
removes that room rather than balancing it: trimmed to cap and baseline the box is
the lettering itself, symmetric padding becomes optical centering by construction, and the
descender hangs with 0.1025em to spare. The pill comes down from 1.70em to 1.40em and the
type sits in the middle of it.
What none of the six demonstrate, and would have to be decided separately: a pending or loading state; a destructive action that has to look dangerous without a second hue; a button label that wraps to two lines; a count or badge riding the enclosure; a split button; and an HDR chosen state on an enclosure rather than on a mark — the row below shows the mechanism, and no family assumes it.
Not a seventh family — a mechanism, and one that already
ships. Active is the only state that leaves SDR. The ladder spends everything getting
to chosen — faint to quiet to full, GRAD 50 to 100 to 150 — and then has
nowhere left to go on a screen that can go further. So the chosen mark is painted with a
flat PQ white instead of with ink. src/icon.css does it to the mark;
src/AxisSlider.css and src/AxisTriplet.css do it to the stepper
chevron. font-proofer's own source carries none of it — it arrives through
shared/ — so there is nothing here that is a port. The swatch below was
read out of icon.css when this page was written, not retyped, and it is
byte-identical in all three primitives.
The swatch
One pixel of field-247, its brightest — about 1216 nits through the PQ EOTF, and literally the same light the poster puts on its headline. Deliberately not PQ 1.0: that is 10,000 nits, which no display resolves, so the system tone-maps it — and clipping is where a bright white turns chalky and shifts hue. Sampling the field's own peak also means a mark glows at exactly the wordmark's white rather than outshining the logotype from 20px. If these two squares are the same color, your screen is SDR and everything below will read as plain full ink.
Cut A — the glyph is the mask
color: transparent plus
background-clip: text makes the engine rasterize the letterform as a
mask rather than as smoothed colored text. That is also why a chosen mark reads
heavier than its GRAD implies — a mask is full grayscale coverage, more actual
pixels, not bloom. Two layers make the boost a slope: ink on top at alpha
(1 − hdr), HDR white underneath. At 0 the ink is opaque and the mark looks exactly
as it did, which is why the handover is invisible. The paint itself cannot be
interpolated, so only that one registered number animates.
Cut B — a shape is the mask
Same swatch, different cut: here it is the paint, and an SVG
chevron masks it — so the thing that lights up is the thing you pressed, rather
than a second shape appearing in a 10×6 box that already holds a chevron.
Momentary is two durations, not an animation: opacity snaps on at
:active with duration 0 and eases back out, so the flash cannot lag the
press or be re-triggered mid-decay by a repeat from the hold timer. The mask is drawn at
the chevron's hover stroke, because a press almost always arrives through a
hover and a mask thinner than the stroke under it leaves a rim of ink around the light.
The light gate
On a light ground the whole thing is unwound: clip back to border-box so the glyph is colored text again and not a mask, the number to 0, the range limit back to standard. Physics rather than taste — the boost exists only for pixels brighter than SDR white, and there is no darker-than-black to boost into. Left ungated, the swatch painted the chosen mark white on a light page: the faintest thing in the row instead of the fullest, which is the exact inverse of what active means.
What this does not settle. It is a technique for marks. Every family above decides a chosen state in ink, ground or edge, and none of them assumes a screen that goes past white. Putting an HDR chosen state on a button enclosure is a design question in its own right, not a port of this — so it is shown here and deliberately left out of all six.
One column matters more than the others. At rest is whether the thing announces itself with nobody pointing at anything — which is the property the brief asks for, and the one a hover state cannot supply.
| family | at rest | icon-only | dense menu | 44px | no corner-shape | editable mark |
|---|---|---|---|---|---|---|
| 01 Keyline | states itself | yes | tight | padding | plain radius | one edge of the box |
| 02 Lozenge | states itself | yes | wide | native | pill is a pill | a wash at 5% |
| 03 Rule | only if nothing else is underlined | none | reads as prose | invisible padding | unaffected | the same rule, dashed |
| 04 Plate | strongest | yes | tight | padding | shadow breaks the corner | a well: the hole |
| 05 Bracket | quiet but unmistakable | yes | 9px ticks read as noise | padding | degrades to 01 | brackets around the words |
| 06 Menu | nothing, unless the row keeps its mark and its width | bare mark | it IS the menu | row height | grounds, not corners | the rule the dial ships |
Two of the six are already half-built. 06’s editable mark is
not a candidate at all — it is .slider-number, shipping, in every dial in
every app, and choosing anything else for editable text means the menu and the field
disagree inside one panel. 01’s enclosure is src/button.css,
likewise shipping. Those two fit together without being reconciled: 06 for the menu, where
an enclosure means chosen and the row does the pointing, and 01 for everything
outside it, where an enclosure means pressable. What that pairing has to survive is
one panel holding both meanings of a box at once, which is a real risk and is the thing to
test before it is adopted rather than after.
02 keeps the two jobs it already has — the one primary action, and one-of-N — as a variant, not a rival. 04 is the strongest at rest and is blocked by the engine, not by taste. 05 is the one worth keeping drawn for a hero or an empty state, where a single button can afford ceremony a dense menu cannot. 03 is the one to reject on purpose rather than by omission: it is the most typographic answer here and it fails exactly where the brief says not to.