Preset tile

One entry of the service catalogue on Add a Subscription: a merchant mark, the name, and what people usually pay. Tapping one turns the form underneath from data entry into a confirmation, which is the whole reason the catalogue is shown rather than counted.

Molecule2 grey pages12 tiles2 axes3 states, both themes

Anatomy

Two atoms in one target, which is what makes it a molecule. Everything below is drawn by design/system/components/preset-tile.css and the atom files it hosts, through the same entry point a product screen loads.

Variants and sizes

Two axes and no size axis: every tile in the product is 56px tall and its width comes from the grid it stands in, two columns at 360px and three past a 760px container. The second axis is the one the inventory does not list, and it is real: the loading state renders the same tile as a span.

Selection

Exactly one preset can be chosen, because choosing one fills the form below it. Nothing in the product renders two pressed tiles, and a second one would mean two different subscriptions were being added at once

Tokens Where it stands
Not chosen .tile --bg-surface
--line-control
11 of the 12. The catalogue as it arrives
Chosen [aria-pressed="true"] --bg-selected
--line-selected
1 of the 12. The current selection, which is the second of the four jobs the One Voice Rule lets petrol do
Chosen, with a petrol name - Forbidden with a reason rather than missing: the selection paints the fill and the edge and leaves the ink alone. Turning the merchant name petrol would spend the accent on a name rather than on the choice, and the six tiles would then read as six different kinds of thing
Element

A button when there is something to press, a span when there is not. The CSS is one rule for both, because a skeleton tile has to occupy exactly the space the real one will or the catalogue jumps when it arrives

Difference Where it stands
button none in the CSS 6 tiles on Add a Subscription. Real targets, in the tab order, with aria-pressed
span, holding skeletons none in the CSS 6 tiles on the loading state, inside an aria-hidden grid. Not targets, not in the tab order, and nothing announced: there is nothing to choose yet

The skeleton form keeps cursor: pointer, and that is the one thing about it worth fixing later. It comes from the shared rule and the element cannot be pressed, so the pointer promises something for the length of a load. It is left alone here because this stage may not move a pixel, and a cursor is on the page as a backlog line rather than corrected quietly.

When to use it

A person meets this at the top of Add a Subscription, which is the manual path D2 makes an equal second door. The catalogue is there as evidence rather than as a boast: a blank form is the version people abandon, and six recognisable marks say "this will take a moment, not an evening". The quiet exit under it says the rest out loud: "Can't find it? Add it by hand and fill the details yourself."

Its copy is a name and a fact, never a pitch. "Netflix" and "Typically $17.99 a month", from voice/docs/microcopy.md. The price is the one people usually pay and it is labelled "Typically" for exactly that reason: the tile is a shortcut into a form, and every value it fills in stays editable.

The rule, and the anti-rule

Selection is an attribute

aria-pressed="true" is both the state a screen reader announces and the selector the tint is painted from. One fact, one place. It is also why this component and the range picker are hooked the same way: two selections in one product should not be built two different ways.

Never a class the assistive tree cannot see
<button class="tile is-selected">

A .is-selected class paints the tile and tells nobody. The two would then have to be kept in step by hand forever, and the first screen that forgets the attribute is a screen where a person using a screen reader chooses a preset and hears nothing change. The rule is not "prefer ARIA": it is that the visible state and the announced state must be the same fact, and only the attribute can be both.

Use a preset tile

A catalogue entry a person picks from a set they can see all of at once, where picking it fills something in rather than going somewhere. The mark carries the recognition and the price line carries the reassurance.

Use a subscription row instead
Netflix   $17.99   Next payment Aug 3

Something a person already has, standing in a list, is the subscription row: no edge, no fill and no corner, because rows stack inside one bordered container and a list of cards is a list nobody can scan. The tile is bordered precisely because it is not a list: it is a set of offers in a grid, and each one has to look like a thing you can press. The row is a molecule of its own, so what stands here is its content drawn by the logo and muted line atoms alone.

Limits

U12, context. A state change is colour over --dur-state, and a component declares a transition only where its host is not a native interactive element. base.css puts the one transition on :where(a, button, input, select, textarea, summary, label) and moves four properties: background-color, border-color, color, text-decoration-color. Nothing on that list moves an object, and the focus ring is deliberately off it. This component is one of the three the rule names, and it declares the transition itself because its host is a button six times and a span ten: border-color and background-color, reading the same two tokens so the two can never drift. Full table with sources and how to check: Usage rules, and the register is on Motion.

U13, context. PRESS. The tile goes down by one nudge while it is held, and its merchant mark lifts on hover. Both are scoped to the button form, so the loading form answers nothing. Everything in this product that moves travels exactly one --nudge, which is 2px, and nothing anywhere travels two. Hover and release run at --dur-state (150ms) on --ease-arrive; a press runs at --dur-press (90ms), because it answers a finger that is already down. The full language and the live specimens are on Motion; the table with sources is Usage rules.

States

Three states, and they exist because a tile is a real button. Each one is a token override rather than a style of its own. There is no disabled form: no tile on any page in either folder carries the attribute, and authoring one would document something the product cannot do. If a preset ever becomes unavailable, that is a new decision and it takes the range picker's disabled paint, which already exists.

The states FADE since 2026-08-16, and this component declares that itself. The tile stands as a button six times, on add-subscription.html, and as a span ten times, six of them on that screen’s loading state and four on the landing. Inheriting the element default base.css carries would fade one form and cut the other: one class behaving two ways, and invisible in a screenshot. The ring is excluded on purpose and arrives instantly: a keyboard user tabbing through six identical boxes moves faster than 150ms per stop. The rule and its tokens are in Limits above.

Hover is declared before selection, and that order is the decision. Both rules are the same specificity, so the later one wins on a tile that is both, and the chosen tile does not repaint under the pointer. The pointer is asking what happens if I press this, and for the chosen preset the answer is nothing new.

Rest and chosen, live

Both are real buttons with real attributes. Hover the second one, then tab to it: the ring is the only thing that tells a keyboard user which of six identical boxes they are standing on, because the tiles carry no other position marker.

The four cells, in both themes

The states are described, not photographed. Founder's decision, 2026-08-11: the snapshot strip went from every page in this kit, 120 images across 18 pages. They were framed as measurements rather than illustrations, and that framing was honest, but the bill came due all session: a value moves, and every picture of it is quietly stale until somebody re-shoots it. A stale picture is worse than no picture, because it looks checked. What is left is what a picture never carried anyway: the state, and the token it reads. The resting component is live and interactive above, so hover it, tab to it, and read the value off the real thing rather than off a photograph of it.

Restlight
--bg-surface, --line-control
Restdark
the same roles, the other half of each pair
Hoverlight
--bg-hover, --line-control-hover. The edge STRENGTHENS: 3.46:1 at rest, 5.78:1 here
Hoverdark
the fill goes LIGHTER here and darker in the light theme: a hover moves away from its ground
Focus visiblelight
--color-focus, 6.23:1 as a line
Focus visibledark
--color-focus, 6.82:1. Offset by 2px so it never sits on the tile's own edge
Chosenlight
--bg-selected, --line-selected, and the ink deliberately unchanged
Chosendark
the tint is a DEEP petrol here rather than a near-white one: a tint has to sit on its own ground, so it is not the light value reused
Retake, and this is the whole recipe, because there is no node Playwright in this repo: serve the repo, then for theme in (none, data-theme="dark" on <html>): goto /design/kit/preset-tile.html move the pointer away -> screenshot #live-rest -> screens/preset-tile-rest-<theme>.png hover #shot-rest -> screenshot #live-rest -> screens/preset-tile-hover-<theme>.png focus #shot-rest, press Tab, assert :focus-visible -> preset-tile-focus-<theme>.png blur, crop #shot-pressed -> preset-tile-pressed-<theme>.png Chosen is a variant rather than a state and it is shot anyway, because it is the cell a person actually comes to this page to compare against rest.

The technical half

ReadsWhich tokenGrowing from
the resting fill--bg-surface--paper
the resting edge--line-control--edge. A tile is identified BY its edge, so it owes 3:1 and gets 3.46:1. DESIGN.md names the preset tile on the control side of that split
the chosen fill and edge--bg-selected, --line-selected--petrol-tint, --petrol
hover fill and edge--bg-hover, --line-control-hover--panel, --muted
the focus ring--color-focus--petrol
the gap and the padding--space-8primitive: a spacing has nothing to override
corner and height--radius-sm, --rowprimitive, straight
the inknone: color: inheritthe merchant name takes the page's own primary ink, so the tile has no ink role of its own

Lives in design/system/components/preset-tile.css. Stands on 2 grey pages and both of their coloured copies: Add a Subscription and its loading state. The grid around it (.grid, two columns and then three) is the Grid organism, and it was .tiles until step 6.

<button type="button" class="tile" aria-pressed="true"> <span class="logo"></span> <span>Netflix<span class="muted">Typically $17.99 a month</span></span> </button> <!-- the loading form: a span, inside an aria-hidden grid --> <span class="tile"> <span class="logo"></span> <span><span class="skel line w60"></span><span class="skel line w90"></span></span> </span>