Guidelines
Every other page here answers how does this component work. This one answers the question before it: which component, and what a set of them owes each other. Nothing on this page is enforced by the CSS, which is exactly why it is written down.
One choice out of several
The control follows from what the choice is, not from how many options there are. The count is a constraint and it arrives last: it can take an answer away, it can never pick one. A rule keyed on the number is easy to apply and gives the wrong answer, which is worse than a rule that asks for a judgement — because it gets applied with confidence.
The first row is a gate rather than an answer. Past it, read down until one fits: the first that does is the answer.
| Ask | Then | Why |
|---|---|---|
| Can two answers be true at once? | not this table | Answer this first: it decides the shape, and the table below only decides the control. «Departures or arrivals» is two views of one page, so it is one answer and the table applies. «Actors and extras» is one search with two conditions, so it is several, and the section under the table applies instead. |
| Is it one thing being turned on or off? | switch, or checkbox | A switch when it takes effect as you press it, a checkbox when it takes effect on submit. Not a two-option strip: «Yes / No» drawn as two buttons is a state wearing the clothes of a choice, and it reads as two things you could pick when there is only one thing you could change. |
| Is it a position on a scale? | range, or number input | Never buckets. Cutting a continuum into «short / medium / long» throws away the value the reader actually wants and then makes them guess which bucket contains it. |
| Is the difference something you have to see? | choice card, or colour input | A colour, a layout, a shape. The control has to show the thing rather than its name — a list of colour names is a colour picker nobody can use, and it is correct by the count and useless in the hand. |
| Does the reader have to see the options to decide? | segmented, or chip group | Two cases: they are comparing, or they do not yet know what is on offer. Either way every option is on screen and nothing is behind a press. This is why the docs' own light / dark / system control is a strip and not a select: a control that hides two of its three states does not read as a control. |
| Does the reader arrive already knowing the answer? | combobox | Typing three letters beats browsing, and it beats it at eight options as much as at eight hundred. The test is whether they can say the thing before the control is open. |
| Is it a closed set the reader can recite? | select | Months, hours, the twenty regions. «Recite» is the test: they do not need to read the list to know what is in it, so hiding it costs nothing, and a native popup is the fastest thing on a phone. |
Now the count, and only to rule things out. Whatever the answer above was, it has to fit at 320px with the text at 200%. The two controls that show every option fail there in opposite ways, and neither failure is visible on a wide screen: segmented never wraps — it scrolls sideways, which hides options behind a gesture nobody makes — while chip group wraps, and past two rows a control that wraps stops reading as one control. So a set that has to be seen and does not fit is a chip group, not a strip; and one that fits on a line either way is a strip. If the answer was «they must see the options» and the options do not fit, the choice is not between controls — it is that the set is too big to be seen, and something upstream has to give: group it, filter it, or accept that the reader will be typing.
A row of separate buttons is the wrong answer at every size and for every kind of choice. Buttons are actions, and a set of them where only one may be picked says nothing about the exclusivity that is the whole point.
Several at once has no "all" option
The question that decides the whole shape comes before the table: is choosing two answers together a sensible answer? Departures or arrivals is not — they are two views of one page, so it is one answer and the table applies. Actors and extras is — it is one search with two conditions, so it is several, and the rest of this section applies instead.
An "all" option belongs to exactly one of the two, and the asymmetry is the part that gets missed. With radios it is compulsory: a radio group has no "nothing chosen" state a person can reach, so without it the reader can enter the filter and never leave it. Write it as a real option with an empty value — that is what makes clearing the filter something to press rather than something only the code can do.
With checkboxes it is a defect. Nothing ticked already means all of them, so an "All" box is a button wearing a checkbox: it is announced as checked for a state nobody chose, and unticking it would have to clear its siblings, which is not what any checkbox does.
Which is why a multiple filter owes the reader two exits in its place: a tag for each active filter, whose ✕ removes its own, and one "Clear filters" that removes them all. Dropping the "All" box without adding those leaves the reader inside a filter with no way out, which is the trap the box was covering.
Two sites in one family filtered the same question two ways — one with radios and an "All" chip, one with checkboxes and none. Each is defensible on its own page. Together they teach one reader two grammars for one question, which is what this page exists to stop.
A label with a count is written once
Options carry their counts, because without them "Orchestra" and "Other" look like two choices of the same size when one holds a hundred and the other holds seven — and the reader who picks the small one thinks they are narrowing a long list and finds almost nothing.
The shape of that label is one function, called by every control that shows the set: select, chip group, and the buttons and links that offer the same set somewhere else on the page. Two shapes for the same fact read as two different facts, and a set of controls is exactly where the second shape gets written.
const withCount = (label, n) => `${label} (${n})`;
withCount('Orchestra', 115); // Orchestra (115) — the select option
withCount('Orchestra', 115); // Orchestra (115) — the chip
withCount('Orchestra', 115); // Orchestra (115) — the link on the empty pageAnd the number goes inside the label, never beside it. The label is the accessible name: put the count in a badge next to the chip and the option is named "Orchestra" while the 115 is announced as a number belonging to nothing. An option cannot hold a badge at all, so the control that can and the control that cannot would disagree by construction.
<!-- Two shapes for the same fact: they read as two different facts. -->
<option value="orchestra">Orchestra: 115</option>
<label class="chip" for="k-orchestra">Orchestra (115)</label>
<!-- And a number outside the label is a number with nothing attached to it:
the option is named "Orchestra" and the 115 is announced on its own. -->
<label class="chip" for="k-other">Other</label>
<span class="badge">7</span>A coloured edge has no radius
When a box carries a bar of colour down one side to say what kind of box it is, that side is square. A 3px stroke bent around a 12px corner stops reading as the edge of the panel and starts reading as a stripe laid on top of it, and the two are a different object.
This is what alert, validation summary and toast already do, and it is the rule whenever a card, a row or a panel is marked the same way. The other three corners keep the radius.
All three write four longhand corners and no shorthand, which is the answer to take: it has no order to get wrong. The rest of this section is for the case where the shorthand is already there — a card, a row, a panel of your own that is being marked after the fact.
Which leaves the half that actually costs the time: the two overrides go after the shorthand. border-radius is a shorthand for all four corners, so written below them it puts back exactly what they took off — and nothing anywhere reports it. The rule looks right, the corners are round, and the bar bends.
.card-flagged {
border-radius: var(--radius-box);
border-inline-start: 3px solid var(--color-primary);
/* After the shorthand. Before it, these two are silently overwritten. */
border-start-start-radius: 0;
border-end-start-radius: 0;
}.card-flagged {
/* Nothing here survives. The shorthand below re-rounds both corners and
no build, no linter and no browser reports it. */
border-start-start-radius: 0;
border-end-start-radius: 0;
border-radius: var(--radius-box);
border-inline-start: 3px solid var(--color-primary);
}A heading never outweighs the heading above it
An h1 set light and an h2 set bold below it inverts the outline the markup declares, and a reader scanning the page follows the weight, not the tag. If the light setting is the one you want, it belongs to the h1 first and everything under it follows.
Size and weight are one decision taken twice. Pick the pair for the top of the page, then step both down together. The practical form of it:nothing else in the outline is heavier than the page title, and below the title the outline is read off the size alone.
In the outline is the load-bearing part, and it used to say «on the page», which is a different rule and one nothing can keep. A label on a box — a card's title, an alert's, a badge, the caption above an example — is not a level of the outline even when it is written as one. It sits at body size and one step of weight above it, because what it has to separate itself from is the paragraph beside it, not the section above it. Twenty-eight components in this library ship semibold for exactly that reason, so a rule that counted them would be a rule broken by every page that opens a card.
What the rule does count is every heading that divides the page itself. Measured across the eight sites built with this library on 17 August 2026, one failed there: an h2 indexing a list of regions, set at 600 under a title at 500 — made heavy to look like a heading, which is the thing its size was already doing.
The same measurement turned up the case the carve-out did not cover, on two sites that had each arrived at it alone: a heading set smaller than the body text has to carry weight, because size cannot carry it downwards. A sticky month strip over a list of events, a route label in a diagram — both below body size, both semibold, and both unreadable as headings without it. They are not exceptions to wriggle through: if a heading has to be smaller than the copy it introduces, ask first whether it is a heading or a label, and if it is a label give it the box that makes it one.
One thing to know before you copy it: the library ships no page-level heading class. Every title it does ship belongs to something — card-title, dialog-title, hero-title — and prose sets the headings inside a body of copy. The title of a page and the titles of its sections belong to no component, so they are two rules you write once in your own stylesheet, which is also why nothing can check them for you.
A link that leaves the site says so
target="_blank", and a warning the reader gets before pressing it. A reader who followed a source link and lost the listing they had filtered has lost their work, and nothing on the page warned them.
The warning is two things, not one. link draws the mark, which covers whoever is looking at it; it is drawn with a mask and therefore carries no name, so a link that leaves the site also needs the words — <span class="sr-only">opens in a new tab</span> inside the link. A mark alone tells half the readers.
Add rel="nofollow" as well when the link is user-supplied or comes out of a feed you do not control. rel="noopener" is no longer part of this: every current engine implies it for target="_blank", and has since 2021. Write it only if you still serve browsers older than that.
The second time, if it is the same thing
The failure this prevents is not duplication, it is divergence: a badge for a metro line came out square in the header, round under the page title and a pill in the list, on one site, in one afternoon, because each was written where it was needed.
Which is why «wait for the third» is the wrong trigger, and it used to be the one written here. All three of those were drawn before anybody counted; by the time a shape has been drawn three times the divergence has already happened, and what is left is not extraction but reconciliation — three shapes and an argument about which was right.The second occurrence is the moment, and the question is not how many times it has been drawn but whether it is the same thing.
That question does the work the count was pretending to do. Two shapes that look alike and mean different things are two components, and merging them is how a change to one silently changes the other. Two that look different and mean the same thing are one component with a variant, and they are the pair worth finding — they are already diverging, and nothing about their appearance says so.
It does not follow that it belongs here. A component belongs in this library when it is about a kind of thing — a badge, a field, a list row. It stays in the project when it is about that project's subject: a badge that knows the colours of a transport network is the network's knowledge, not the library's. Keep it local, give it a name, and give it one file.
A filter shows what it is filtering
A field at the top of a long list scrolls away, and with it every signal that the page is showing a subset. Two things fix it, and the second one matters more than the first: keep the field reachable, and put the count and the active filters immediately above the results they produced.
Not in a band of their own with space around them. Attached to the list, as one block, so that "127 results" and the first row are read together.
The space between two things belongs to what contains them
A component in this library carries no outer margin, and it cannot: a card does not know whether the next thing is another card, a heading, or the end of the page, and a component that guesses is a component that is wrong somewhere. Every design system worth copying has that rule.
It has a second half, and it is the half that gets dropped: if the component does not own the gap, something has to. That something is the container, and stack is how you say it. Where there was no such thing, the gap became the page's problem and was answered one element at a time — 318 hand-written margins across the nine consumers of this library, in nine stylesheets, with values that drifted apart, on 17 August 2026.
The gap is always a margin, never block padding. The two are not interchangeable and only one of them composes: adjacent margins collapse to the larger of the pair, while padding is the one piece of vertical space in CSS that never collapses with anything. Put the gap in a padding and every neighbour's margin adds to it instead of resolving with it. Measured on a published page on 19 August 2026: a note's padding and its column's padding stacked into a gap nobody could find by reading, because each rule on its own was right.
The corollary catches people out: a border stops margins collapsing through it, so a block with a rule on top has no way to keep its text off that line except a padding — and then the adding starts. Where a block needs a line above it, the line is an <hr class="divider"> of its own, which has margins on both sides and collapses like everything else. note lost its border to this rule and is the shortest example of it.
That is the block axis. The inline one has its own primitive and its own way of failing: a row that has to wrap is cluster, and the two declarations nobody remembers are flex-wrap: wrap and min-inline-size: 0. Their absence is every sideways-scrolling bar this project has logged, and it was the more common of the two shapes written by hand.
/* Each of these is one component putting a gap back that the reset took off,
in a stylesheet that knows nothing about the next one. Counted across the
nine consumers of this library on 17 August 2026: 318 of them, in nine
stylesheets, with values that had drifted apart. */
.station-place { margin: calc(var(--space-2) * -1) 0 var(--space-5); }
.board-heading { margin: 0 0 var(--space-3); }
.nearby-note { margin-block-start: var(--space-6); }<!-- The container says it once, and it holds for whatever goes in. -->
<div class="stack">
<h1>Milano Centrale</h1>
<p>In Milano, Lombardy.</p>
<div class="segmented">…</div>
</div>The size is picked from the rhythm scale, not invented. Four steps, named for the job: --rhythm-page (100) between two major regions, --rhythm-section (50) between two sections, --rhythm-block (24) between two blocks inside a section, and --rhythm-line (16) between two lines of one block. stack, stack-tight and stack-loose are the last three. They are in rem, so a reader at 200% text gets the rhythm doubled with the type rather than a page of doubled words in the old gaps: measured on this page, the gap between two sections is 1,54 times the body leading at 1280px, at 390px, and at 320px with the text doubled.
The exception does not need a second stack. Set --stack-space on the child, and it changes the space above that child. Write it on the child even when what you mean is the whole container — a gap token on the container is read off the child, so a child that is itself a container redeclares the very property its parent's rule reads for it. That is not a hypothetical: it was three separate bugs in one day, and it is why --stack-space is registered @property … { inherits: false } and why the old --stack-gap no longer exists.
/* The one child that needs more room says so itself, and the gap it changes
is the one above it. This is what a hand-written margin was for; unlike a
margin it cannot go negative behind your back, and it cannot double up with
the margin of the element above. */
.site-note { --stack-space: var(--rhythm-section); }
/* And a whole container that wants a rhythm of its own writes the same
property on its children. Not on itself: the gap is written by the parent's
selector and read off the child, so a token on the container is the one a
child that is itself a stack redeclares. */
.panel > * { --stack-space: var(--rhythm-line); }A negative block margin between two siblings is a bug report, not a value. It says a gap is wrong without saying whether the number is wrong or the element is in the wrong place, and it hides the difference from whoever reads the file next. The honest uses are a different thing: a component negating its own padding, to bleed a child out to the edge of the box the way card, sidebar and menu do, or to pull its own close button into its own corner the way dialog and drawer do. Both are a component reaching inside itself, and both are written in the file that owns the padding. What is a bug report is a margin on a sibling, in a stylesheet that does not own either of them.
One on a live site pulled a subtitle eight pixels up into the descenders of its own title, on 4.870 pages in each of two languages, at every viewport — and the CSS gave no sign, because nothing in a stylesheet says where the ink is. A title set with tight leading has glyphs that reach past the bottom of its own line box, so a gap of zero is already an overlap and the correction made it worse. Measure it in a browser or it is not measured.
One more thing that hides: a two-class selector in the page — .page p — outweighs a component's own single class and silently zeroes a gap that class declared. The gap in question was deliberate, commented and reviewed, and was missing from half the pages that declared it. If a rule of yours names an element inside a component, scope it with > or take it back to the container.
A band gets its breathing room from the column inside it
A band is a full-width region of a page — a hero, a strip of colour, the footer. It runs edge to edge and the reading column lives inside it, so the band itself is not the thing that is centred. That much is layout. The part worth writing down is where its vertical space comes from: not from the band, and not from a padding. From the column.
<!-- Nothing here declares a padding. The column's margin is the whole system:
two bare bands sit 50 apart because adjacent margins collapse to max(),
and the painted band traps the same margin and reads it as its own room. -->
<section class="band">
<div class="center"><h2 class="section-title">Departures</h2></div>
</section>
<section class="band band-fill">
<div class="center"><h2 class="section-title">Strikes this week</h2></div>
</section>The column carries a margin on both sides, and adjacent margins collapse to the larger of the pair rather than adding up. So one number produces every gap on the page, and the three cases are these: two bare bands sit 50 apart, not 100; a bare band next to a painted one gives 100, which is 50 of empty page and then 50 of breathing room inside the paint — the band got its padding from the margin it trapped, with nobody declaring one; and two painted bands touch at 0, with their text 50 from each edge. The rhythm is uniform on the painted edges rather than on the text, and the painted edge is what the eye follows.
Containment is the exception, not the rule. It is tempting to put flow-root on every section and be done with it, and it switches the mechanism off: if nothing lets a margin escape, nothing collapses, and two bare bands come out at 50 + 50 rather than at 50. A band contains when it paints or draws a border — because there the trapped margin is what becomes the room inside — and stays open otherwise. A hero paints, so it contains itself and needs neither class: class="hero band" is the whole of it.
Painting is a class, and it is the same class that contains. band-fill sets the background and display: flow-root together, because a band that does the first without the second is the one broken case in this design — and it does not read as broken:
/* The one broken case, and it does not look broken. The paint starts where the
margin ended, so the gap above the band and the room inside it are the same
50px of colour and the text sits on the top edge. Adding a padding-block is
worse: it stops the collapse, and the two add up — measured at 88px against
the 40 of the band beside it. */
.band-promo {
background-color: var(--color-surface-raised);
padding-block: var(--space-8);
}There is one more place this can be switched off without a symptom, and it is worth knowing because eight sites had it: a band that is a direct child of a flex or grid container — including frame, the document frame — is not in normal flow, so nothing collapses and 50 comes out as 100. Put the bands inside the frame's main, which is a flex item and therefore a formatting context of its own.
A rule spans the whole thing it divides
A border on the block edge with no borders on the inline edges is not decoration, it is a statement: everything below this line is a new section. So it has to reach both edges of the thing it is dividing. A rule that stops halfway across the column does not read as a divider — it reads as a border somebody forgot to finish.
It goes wrong for one reason, and always the same one: the element carrying the rule is also the element carrying a max-inline-size. Those are answers to two different questions — how wide is the thing being divided, and how long may a line of prose be — and one element cannot hold both.
/* One element, two jobs. The cap is for the prose and the border is for the
section — so the border gets capped too, and the section divider stops in
the middle of the page. Measured on a live site: 480px inside 1056. */
.note {
max-inline-size: var(--measure);
border-block-start: var(--border-width) solid var(--color-border-subtle);
padding-block: var(--space-6);
}/* The rule belongs to the container, the measure to the text. */
.note {
border-block-start: var(--border-width) solid var(--color-border-subtle);
padding-block: var(--space-6);
}
.note > * {
max-inline-size: var(--measure);
}A box is different, and this rule does not touch it. A panel with four borders is allowed to be narrower than the page, because a box is an object on the page rather than a division of it. What cannot be narrow is the open line.
If it could ever have two paragraphs, it is a container
Padding, a border and a ground belong to a box. The moment they are put on a <p>, the element is a leaf that has been asked to be a frame, and it renders perfectly right up to the day the copy grows a second sentence that wants its own line.
<!-- Already wrong, and it renders. The frame lands on the leaf, so the day a
second paragraph arrives the tag changes and every rule has to be re-aimed
— and today the measure is missing, because note puts it on the children a
<p> does not have. -->
<p class="note">Times come from the operator's own board.</p><footer class="note">
<p>Times come from the operator's own board.</p>
</footer>The cost is one element today and it is paid back the first time the content changes: the tag stays, the rules stay, and a second paragraph is one more line of markup. The leaf version has to be taken apart and rebuilt instead — and it is not even right today, because a component that puts the frame on itself and the measure on its children gives a childless element the frame and nothing else.
The test is not "does it have two paragraphs", it is could it. Anything holding copy somebody will edit — a note, an empty state, a panel, a card body — is a container. note is this rule made into a component.
--measure is the lede's width, not the body's
A page of prose has two widths, and only one of them is --measure. The lede — the single grey sentence under the title that says what the page is — is set a step bigger and a good deal narrower than the text under it, and that pair is what makes a reader see it as a lede instead of as the first paragraph. Running text is wider: the page column, capped so that a line does not run past what an eye can track back.
The numbers, so this is decidable and not a warning: --measure is the lede, and 44rem is the body. The second is not invented here — it is what the eight sites built with this library render, 704px of text inside a 736px page, and those are the pages people read. It is a literal rather than a token today, which is the honest state of it: the library ships one measure and the second width lives in each consumer's own stylesheet.
And the unit is why it goes wrong on its own. --measure is 48ch, and a ch is the width of a zero in the type of the element it is written on. So the same declaration on a lede at 18px and on body copy at 16 produces 576px and 528: the copy comes out narrower than the sentence introducing it, and it does so by construction rather than by anybody's mistake. Two of the eight sites had exactly that, in the same shape, found on the same day by two people who did not know about each other. A width for a column is a length; only a width for a line of type is a ch.
Capping every paragraph at --measure is the mistake, and it does not look like one while you are writing the CSS. It looks like care. What it produces is a ribbon of text a third of the way into an empty column, with the lede indistinguishable from the seventeen paragraphs beneath it — measured on this page and on skins on 16 August 2026, where the column and every paragraph in it were the same 511px.
Two things this rule does not cover. A caption on a box — an example note, a figure caption — lines up with its box and not with the text measure, because a caption that is narrower than the thing it describes hangs off its edge. And a table or a specimen is as wide as it needs to be: the cap is for prose, which is the only thing an eye reads line by line.
A material is the control layer, never the content
A material is a surface that lets what is behind it show through, and the reason to pay for one is not that frosted glass is pretty. It is that a fixed bar has one thing to communicate before it communicates anything else — the page is scrolling underneath me, not behind a wall — and translucency is the only cheap way to say it. Apple's guidelines put the general form in one sentence: by letting colour pass from background to foreground, a material establishes hierarchy and helps people keep a sense of place.
So the rule is about which layer, not about taste. Chrome that floats over the page — a navbar, a sidebar, a toolbar, a popover, a menu, a toast — takes a material. Content does not. A card, a panel, a table row and a stage are the thing being read; giving one a material stops it separating anything and only makes its text harder to read against whatever happens to be behind it.
The thickness is a second question with its own answer, and the trade is stated rather than felt: a thicker material is more opaque and gives better contrast for fine features; a thinner one keeps more of the reader's context. So it follows the amount of small text sitting on it — --material-thick for a sidebar, which is two dozen small labels; --material-regular for a menu, a sheet or a bar, which is the default; --material-thin and --material-ultra-thin only where the thing behind is worth seeing, which in practice means over media.
Opting in is two declarations — a background of var(--material-regular) and backdrop-filter: var(--material-filter) — and you write no @supports and no media query. The four conditions that take the glass away again — no backdrop-filter, prefers-reduced-transparency, prefers-contrast: more and [data-legacy] — are decided once, in tokens/materials.css. A component that rolls its own has quietly opted out of three of them, and nothing will look wrong on the machine it was written on.
Arriving springs; leaving does not
The library's easings are springs rather than béziers, and the difference is at the end of the movement: a bézier arrives dead, a spring arrives settling. Four of them, named for what they are for — --ease-quick for a press or a hover, --ease-snappy for something arriving, --ease-smooth for something leaving, and --ease-bouncy, which is the loudest thing in the library and should be rare.
The half that is easy to get wrong is not which curve, it is where you declare it. A transition takes its timing function from the state it is animating to, so the rule on the open state governs the entry and the rule on the closed one governs the exit. Declare only the second — which is the natural way to write it — and the same curve runs in both directions.
That is invisible in one direction and wrong in the other. --ease-smooth is the only one of the four that never crosses its target; every other one overshoots, which is what gives an arriving panel mass and what pushes a leaving panel back onto the screen for a frame or two after it should have gone. On a sheet that travels a whole panel width it reads as a bounce against the edge of the screen.
And the general rule underneath both: add motion because it says something. The guidance the library follows is blunt about it — do not add motion for the sake of motion, and never let it be the only way something is communicated. All four durations and all four easings collapse under prefers-reduced-motion, so a component that animates with the tokens honours that for free; one that writes its own duration does not.
A control that can be pressed shows the press
Every custom pressable needs a visible pressed state. Without one a control feels unresponsive, and the thing people do with an unresponsive control is press it again — which is how a form gets submitted twice.
A colour change alone is not enough, and the reason is physical rather than aesthetic: at the moment of contact the thing that changed colour is under a finger. .btn scales by --btn-active-scale instead, because a scale keeps the centre still while a translation moves the label out from under the pointer that is on it — and it is visible at the edges, which are the part not covered.
Two exceptions the library makes and you should copy. A button that anchors an overlay does not scale: anchor positioning resolves against the anchor's border box, so pressing an open popover's invoker shrinks the anchor and slides the panel out from under the finger closing it. And the promotion to its own layer goes on :active rather than :hover — promoting a transparent element costs it subpixel antialiasing, so on hover every outline and quiet button changes weight the instant the pointer touches it.
A screen spends its attention once
Every rule above this one is about a single decision being right. This one is about all of them together, and it is the difference between a screen that is correct and a screen that looks composed: one accent colour, one step of weight, one radius.
Not because three would be ugly. Because attention does not add up. A page has one thing the reader is looking for — the next departure, the button that starts the thing — and everything given a colour, a weight or a shadow is a claim on the same attention that thing needs. Two claims are not twice as loud; they are the same as none, and the reader falls back to reading top to bottom.
The practical form, and each of them is a question with an answer rather than a matter of taste. The accent is --color-primary, and it belongs to what the page is for; a second brand colour on the same screen is two things claiming to be the point. The weight is the page title and nothing else — below it the outline is read off the size, which is the rule two sections up seen from the other end. The radius comes from the skin, so a literal anywhere is a shape that will not follow it.
And the one that removes rather than adds: a component you reached for because it exists is a component to take out. A badge on a row that needed no badge, a card around a paragraph, a toast for something the reader is already looking at — each is correct on its own page and each spends attention on nothing. The question is not whether the component fits; it is what on this screen would be quieter without it.
Nothing found and nothing loaded are different states
An empty list because the filter matched nothing, an empty list because a source did not answer, and an empty list because there is genuinely nothing today are three sentences, not one blank panel. Use empty state and say which one it is, because the reader's next action is different in each case.