Empty block
What a screen says when there is nothing to show yet: a heading, one sentence, and the two doors out. It is the only block in the product whose whole job is to be replaced by content, and the reason it is a component rather than a heading and a paragraph is the tone. An empty state here is never an error, so nothing about it is clay and nothing about it apologises.
Anatomy
Three slots and nothing around them. Everything below is drawn by design/system/components/empty-block.css and by the door's own file, 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.
Two ways to start
See everything you pay for in one calm place. Pick the one that feels right; you can change it later.
Connect your bank
Read-only, through Plaid, and about a minute. Tendd cannot move your money.
Add a subscription
Start with one and add more later. No bank is involved, and nothing leaves your control.
Behaviour at width
It carries the reading measure, --container-text. It was already 52ch, which is what made 52 the mode of the four measures the product had, so the token took this value and this component did not move a pixel at either width. See Width.
.emptythe organism itself, and it is a margin. No fill, no edge, no radius and no padding: an empty state is the screen's own content standing on the page ground, not a box drawn around an absence> h2the block's own heading slot, at--type-sub. It carries no class on either page. Anh2and never anh1: the screen'sh1is above it, so an empty block never opens a screen> pone sentence,--type-body,--text-muted, capped at 52ch. The second tier, used as the second tier>the combinator is the whole architecture of this file. Both rules above are direct-child selectors, because every door inside carries anh3and apof its own. Written as a descendant, this file would reach and repaint two paragraphs it does not own, on both of its pages.grid.roomyan organism. The Grid in its roomy form, one column at 360 and two past a 760 container. This file gives it nothing at all, not even a margin: the block's own bottom margin is the last gap, so there is nothing left to say. It was.doorsuntil step 6.doora molecule. The Door, with its own control edge, its own 16px heading, its own hover and its own focus ring. Not one declaration here touches any of them
Variants and sizes
One form, so there is no matrix. Both occurrences are the same three slots at the same three values, and the only thing that differs between them is the words. There is no tone axis, no size axis and no host axis: the block reads one colour, --text-muted, on one of its three parts, and everything else it needs is inherited or belongs to the grid inside it.
The two in the product, side by side:
| Page | Heading | Sentence | What is missing |
|---|---|---|---|
| home-empty, coloured | Two ways to start | See everything you pay for in one calm place. Pick the one that feels right; you can change it later. | subscriptions, because none have been found yet |
connections-empty, grey | No sources yet | Add one and your subscriptions appear on your list. You can change this later, and you can have both. | sources, which is what would find them |
No sources yet
Add one and your subscriptions appear on your list. You can change this later, and you can have both.
The stand shows this one at the full column, and that is a measurement. The default stage measure is 440px and .wide is 620, and both would be wrong here: on design/home-empty.html at a 1440 viewport the block renders 908px wide, because it is a direct child of .screen and past a 900 container the shell's blanket 780px measure comes off. At 360 it renders 313px, which is the whole screen inside its own padding. So the block genuinely spans its column at both ends of the range, the stage is unbounded to match, and the sentence is held back not by the stage but by its own 52ch.
When to use it
An empty block stands where content will stand, on a screen that has already introduced itself. Home before a bank is connected or a subscription is typed, and Connections before a source exists. Both are the very first thing a person sees after arriving, which is why the tone matters more here than anywhere: this is the audience that closes finance apps, and the first screen has to read as a beginning rather than as a fault.
It says what to do, not what is absent, and the wording was corrected once for exactly that. voice/docs/microcopy.md records the change: home-empty's second heading was "Nothing here yet" and became "Two ways to start", because two headings a hundred pixels apart were doing one job and the first already said what was true. The heading says the move; the sentence says the reward and takes the risk out of it ("you can change it later"); the doors are the move itself. On Connections the same shape runs the other way round, because "No sources yet" IS the move's subject there.
The two doors are equals and the block never picks one. D2 makes manual entry a real second path rather than a fallback, so neither door is filled, neither is first by emphasis, and the sentence says "Pick the one that feels right" instead of recommending. That is the one rule an empty state in this product could most easily break, and it would break it by making one door louder.
The rule, and the anti-rule
Two ways to start
See everything you pay for in one calm place. Pick the one that feels right; you can change it later.
No box, no icon, no illustration and no colour but the muted second tier. The only edges on screen belong to the two doors, which are the thing to press. An empty state that draws its own frame turns an absence into an object, and this is the screen whose job is to make an absence feel like a start.
Still gathering your history
Trends 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.
When a screen has nothing to show because it is waiting rather than unstarted, the component is the Text block. It opens the screen and carries its h1; an empty block sits inside a screen that already has one, and carries an h2. The two are one line apart in the markup and a whole level apart in the heading order, which is how a screen reader tells them apart and how a person does too. History and trends with under three months is the clearest case: nothing is missing that a person could add, so there are no doors and there is no empty block.
No sources yet
Add one and your subscriptions appear on your list. You can change this later, and you can have both.
The sentence is muted and the heading is plain. There is no tone token in this component at all: no amber, no clay, no wash and no badge. DESIGN.md forbids it by name, and the rule is one line: do not use clay on an empty state, because nothing went wrong in an empty state.
We could not refresh just now. Showing your last update from today, 9:14 AM.
Try againWhen something actually failed, the component is the Wash block in its error tone, and that is where the desaturated clay lives. It is a tinted paragraph inside the flow with its own inline link, not a heading and two doors. Reaching for an empty block to carry a failure would make a fault look like a beginning, and dressing an empty block in clay would make a beginning look like a fault. The two mistakes are the same mistake from opposite ends.
States
No states, and the reason is one line: an empty block is not interactive. It is a section on both occurrences, it holds no anchor, no button, no summary, no tabindex and no role of its own, and kit.css gave it no pointer response either. What a person presses inside it is a Door, and the door has hover and focus-visible on its own page, which is where somebody looking for them will go. Faking a state here with a class the product does not carry would document something the product cannot do.
It has no loading form either, and the absence is a decision rather than a gap. A screen that does not yet know whether it is empty shows the loading form of the thing it is waiting for: home-loading renders skeleton rows, not a skeleton empty state. An empty block is what the product says when it knows there is nothing, so a version of it that is still finding out would be a contradiction.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the sentence | --text-muted | --muted, the single secondary tier, and the only colour in the file |
| the heading's size | --type-sub | primitive, straight. 17px folded to 16 at the founder's review of the foundations |
| the sentence's size | --type-body | primitive, straight. 13.5px folded to 14 |
| its three margins | --space-8, --space-16, --space-24 | primitive, straight. 6 folded up to 8 and 18 folded down to 16 |
| no fill, no edge, no radius | none | by decision. The doors draw the only edges on the block |
| the reading measure | 52ch | a character width, which is on neither scale, so it stays the literal it is |
The stand had to clear this component's name before it could be built. _page.css used .empty for the cell that marks a forbidden combination in a matrix, and it was renamed to .kit-empty on 2026-08-11, ahead of this round rather than during it. The collision would have been harmless on the day (no stand cell sits inside an .app scope), and "the code and the stand do not mix" is a rule about NAMES and not only about the cascade: the moment this organism landed, a product rule would have started matching reviewer chrome and nothing would have errored.
Lives in design/system/components/empty-block.css. Stands on 2 grey pages, 1 of them coloured: Home, nothing tracked yet. The second, wireframes/connections-empty.html, is still grey.
<section class="empty">
<h2>Two ways to start</h2>
<p>See everything you pay for in one calm place. Pick the one that feels right; you can change it later.</p>
<div class="grid roomy">
<a class="door" href="connect-bank.html">
<h3>Connect your bank</h3>
<p>Read-only, through Plaid, and about a minute. Tendd cannot move your money.</p>
</a>
<a class="door" href="add-subscription.html">
<h3>Add a subscription</h3>
<p>Start with one and add more later. No bank is involved, and nothing leaves your control.</p>
</a>
</div>
</section>