Text block
A heading and a paragraph, and the container decides how loud both are. One class name in the product, .textblock, which was .lede and .state until step 6, and one anatomy: they were read markup first across every page in both folders, and neither contains a single thing from the kit.
Anatomy
Two content slots in one unit, which is what makes it a molecule rather than an atom: a heading that names the thing and a paragraph that says the one sentence about it. Everything below is drawn by design/system/components/text-block.css through the same entry point a product screen loads, so this page cannot drift from the screens: there is no second copy of the CSS here to drift from.
Add a subscription
Pick from 400+ services, or add your own. Nothing leaves your control.
.textblockthe block at page scope. Its heading names the screen, and there is content below it that still works. It was.lede.textblock.statusthe same anatomy at block scope. Its heading names a condition the screen is in: nothing found, nothing loaded, nothing there yet. It was.stateh1/h2the heading slot. Which tag it is answers the document outline, not the size: see the axis belowpthe sentence slot. It reads--text-body, one tier under the heading, and it is capped by a measure rather than by a layout widthmax-width48ch on the lede, 50ch on the state. A reading measure: past about 50 characters the eye starts losing the start of the next line- no atoms the callouts above name every part there is. No button, no chip, no muted line, no link, on any of the 42 grey pages or the 21 coloured ones. That is the evidence the merge was made from
Behaviour at width
It carries the reading measure, --container-text, on both its forms. They were 48ch on the body and 50ch on the status form, and at stage 10 they folded into the one token at 52ch along with the two others in the system: four numbers for one decision is drift, not precision.
Measured at 1280: a paragraph goes from 424px or 441px to 459px. It is the single most widespread change of stage 10, on 21 of the 32 coloured pages, and it is the whole of the change on 19 of them. Audit rows 9, 10 and 16, wider by air. Nothing moves at 360, where the measure does not bind. See Width.
Variants and sizes
One axis, scope, and three values that survived the type consolidation as three separate steps. That was checked rather than assumed, because the same founder review collapsed the button's size axis to nothing: 24 stays 24, 21 rounds to 20, 17 rounds to 16, nearest step and ties to the larger. No two scopes land on the same one, so every row below is a real difference on screen.
| Scope The CONTAINER decides, and the question is not "how important is this" but "what does this block name". A page, a state that has taken the page over, or a state inside a page whose subject is spent elsewhere |
Tokens | Where it stands |
|---|---|---|
Page .textblock h1 |
--type-head --text-primary |
25 grey pages, 10 coloured. Always an h1, on every one of them: a lede names the screen and a screen has one subject |
Tendd ProPay less per month than most of the subscriptions you will cancel. | ||
Block .textblock.status h1 |
--type-title --text-primary |
16 grey pages, 6 coloured. The state IS the page: History and Trends with nothing to plot yet, the reveal with nothing found, a payment that did not go through |
Still gathering your historyTrends need a few months to be worth looking at, and we have less than three so far. Come back in a few weeks and the shape of your spending will be here. | ||
Inside a block .textblock.status h2 |
--type-sub --text-primary |
2 grey pages, 1 coloured. The page keeps its own h1 above ("Add a subscription") and this names a condition under it |
No match for "Cerebro Cloud"Not every service is in our list yet. You can add it by hand below, and it will sit alongside the rest. | ||
No heading .textblock.status p alone |
--type-body --text-body |
1 grey page, 1 coloured, both of them the detail error. The screen already names the subscription in its hero, so a second heading would name it twice. A form, not an omission |
We could not load the rest of this subscription. This is usually temporary, and nothing about your money changed. | ||
| A lede at the inset scope | - | Forbidden with a reason rather than missing. A lede is the block that names the SCREEN, and a screen has one subject: a second one further down is either a group head or a state. If a page seems to want two ledes, it is two pages, which is the same argument the One Number Rule makes about figures |
The heading level is not the axis, and reading it as one is the mistake this table exists to prevent. The status form is written as an h1 on 16 grey pages and as an h2 on 2, and what decides is the document outline: on add-subscription-empty the page keeps its own h1, so the state's heading has to be an h2 under it. Outline first, size second. The two are separate rules rather than one rule keyed on the tag, which is the same lesson the group head learned the expensive way, twice, before it was bound to its class.
The one thing the consolidation actually folded is the paragraph. The lede's was 14.5px and the state's was 14px, a half step apart for no stated reason, and both land on --type-body. They are one rule now. What stays apart is the measure and the bottom margin, and both are real: a lede's paragraph is the last thing in its block, a state's is followed by an action row on 10 of the 19 grey pages and 4 of the 8 coloured ones.
When to use it
This is the block a person reads before doing anything. At page scope it is the first thing under the app bar on Upgrade, Add a Subscription, Settings and History and Trends: it says what the screen is for in one heading and one sentence, and then gets out of the way. At block scope it is what a screen says when it has nothing to show, which in this product is most of the interesting moments: no history yet, no match for what you typed, nothing loaded, a payment that did not arrive.
Both jobs are the same job at different volumes, which is why they are one component. The product's own rule is that every screen lowers anxiety, and an empty or failed screen is where that is hardest: voice/docs/microcopy.md writes these lines to say what happened and what is true now, in that order. "Still gathering your history" rather than "No data". "Not every service is in our list yet" rather than "No results". A heading here never carries an exclamation mark and never apologises twice.
It is deliberately untoned, including on the error states, and that was decided rather than overlooked. A wash block carries a fill because it sits above content that still works and has to be told apart from it. A text block at state scope IS the screen: there is nothing beside it to be told apart from, the sentence and the Try again button below already say what happened, and a wash around a full-screen message is decoration rather than information. Left aligned for the same reason, at both scopes: a centred message mid-flow reads as a stop rather than as a step.
The rule, and the anti-rule
You
Your details, your plan, and the two screens that hold everything about your data.
Settings, at page scope. The heading is the destination's own name, the sentence says what is behind it, and the whole block is three lines on a phone. The 48ch measure is what keeps it two: the same sentence set to the full width of a desktop column would run to one very long line and read as a paragraph rather than as a caption.
Streaming is up $6 since March. Everything else held steady.
A sentence with no heading over it, qualifying something above it, is a muted line and sits at the second ink tier. Setting it as a text block would put a full ink paragraph mid-screen with nothing naming it, and the eye would read it as the start of a new section that never arrives.
That payment did not go through
Your bank did not approve it, so nothing was charged. This happens most often with a card that has expired or a bank that wants to confirm a new payment.
The failure state on Upgrade. No fill, no border, no icon, no clay: the words carry it. This is the calmest thing a payment failure can look like, and it is the whole reason the block has no tone axis.
Netflix went up by $2.50 on Jul 28. Your next charge is $17.99 instead of $15.49.
When the message sits ABOVE content that still works, it needs to be told apart from it, and that is the wash block: an amber fill on the price change banner, a clay one on our own failure. The test is not how serious the news is, it is whether there is anything else on the screen for the message to be confused with. A wash around a full screen message is decoration; a plain block above a working screen is invisible.
Limits
U4, composition. One .textblock.status per screen. A screen that needs to announce two things is two states, not one screen.
U5, composition. One introductory .textblock per screen, and it is the first block of its column. That is also the contract of the list column pattern.
U6, composition. A status block comes with a way out in the same container, an action row or a grid of doors. Two screens are exempt and both are named in the rules: they are system waits, where a control would offer an action the product does not have. Full table with sources and how to check: Usage rules.
States
None, and it was checked rather than assumed. A text block is a heading and a paragraph: a sweep of every .lede and every .state on all 57 grey pages, and of every .textblock on all 57 coloured ones, 44 blocks, returns zero anchors inside any of them. Nothing here can be hovered, focused or pressed, and the actions that follow a state block are siblings of it rather than children. Drawing four empty cells would document something the product cannot do.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the heading, all three scopes | --text-primary | --ink, and --ink-dark in the dark theme. 17.4:1 on the light surface, 15.0:1 on the dark one |
| the paragraph | --text-body | --slate. 10.2:1 light, 10.4:1 dark |
| page scope | --type-head | 24px, straight from primitive: type has nothing to override |
| block scope | --type-title | 20px. Was 21px, folded by the founder's review of 2026-08-11 |
| inset scope | --type-sub | 16px. Was 17px, same review |
| both paragraphs | --type-body | 14px. The lede's was 14.5 and the state's 14, and this is where they became one rule |
| the block gap, the heading gap, the state paragraph's gap | --space-24, --space-8, --space-16 | the 8px grid. 22 to 24 and 18 to 16 are folds, recorded centrally |
| the measure | literal | 48ch and 50ch. A measure has never been tokenized and --measure is on the deferred list in docs/inventory.md |
Lives in design/system/components/text-block.css. Stands on 43 distinct grey pages and 43 coloured ones, 45 blocks on each side. A sample of the coloured screens carrying it: Add a Subscription, Tendd Pro, Settings, History and Trends and its empty state, the no-match state (which carries both scopes at once), the failed payment, the detail error.
<div class="textblock">
<h1>Add a subscription</h1>
<p>Pick from 400+ services, or add your own. Nothing leaves your control.</p>
</div>
<div class="textblock status" role="status" aria-live="polite">
<h1>Still gathering your history</h1>
<p>Trends need a few months to be worth looking at, and we have less than three so far.</p>
</div>
The role="status" and aria-live="polite" on the state block are markup rather than CSS, and they stand on 26 of the 27 state blocks across both folders: the block appears after something has been tried, so it has to be announced rather than found. Carried into the copyable markup above so it is not lost the next time somebody writes one from memory. The one without it is wireframes/connections-add-source.html, which is still grey and is a backlog line for the rollout rather than a change made here.