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.
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.
.tilethe molecule: abutton, 56px tall, with a control edge and a control corner. It setstext-align: leftandcolor: inheritbecause a button that says nothing centres its text and takes the browser's ink.logothe merchant mark, the logo atom at its 30px tile size. That size lives inlogo.css, not here: it is the atom's own size-by-container axisspan:last-childthe text column, and the only thing allowed to shrink.min-width: 0is load-bearing: without it "Adobe Creative Cloud" pushes the tile wider than its grid track instead of wrapping inside it.pthe price line, the muted line atom at its 12px tile size, also declared in the atom's own file[aria-pressed]which preset is chosen. The attribute is the selector, so what a screen reader announces and what the stylesheet paints are one factmin-heightreads--row, not--tap: a tile is a row-sized object standing in a grid beside other row-sized objects
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 |
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
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.
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.
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.
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.
--bg-surface, --line-control--bg-hover, --line-control-hover. The edge STRENGTHENS: 3.46:1 at rest, 5.78:1 here--color-focus, 6.23:1 as a line--color-focus, 6.82:1. Offset by 2px so it never sits on the tile's own edge--bg-selected, --line-selected, and the ink deliberately unchangedRetake, 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
| Reads | Which token | Growing 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-8 | primitive: a spacing has nothing to override |
| corner and height | --radius-sm, --row | primitive, straight |
| the ink | none: color: inherit | the 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>