Dialog sheet
The three decisions this product asks for on a page of their own. Below a 760px container it is a narrow column with nothing around it; past 760 it becomes a centred card with a hairline, a corner and the only shadow this system owns. It is also the one component that does not do what its name suggests: nothing about it floats, and there is no overlay anywhere in the product.
Anatomy
One surface and whatever the decision needs: a heading, a sentence, the choices, and a way out that is not a choice. Everything below is drawn by design/system/components/dialog-sheet.css and by the hosted components' own files, through the same entry point a product screen loads.
Add a source
The same two ways in, and you can have both. There is no limit on how many you add.
Connect your bank
Read-only, through Plaid, and about a minute. Tendd cannot move your money.
Choose this pathAdd them yourself
Start with one and add more later. No bank is involved, and nothing leaves your control.
Choose this pathBehaviour at width
From --bp-tablet, 47.5rem, the sheet stops being a sheet. On a phone it is a surface that arrives from the bottom edge and belongs to it. Past the point it detaches: it gains a top margin, its own padding, a hairline, the container radius and the one elevation in the system, and becomes a dialog standing in the middle of the pane.
This is the clearest case in the product of a point rather than a fluid rule: an edge-attached sheet and a floating dialog are two behaviours, and there is no value in between to interpolate. The 480px cap it carries at every width is a measure and not a point. See Width.
.sheetthe organism itself. A 480px column, centred, with no fill and no edge below a 760 container; a padded card with all three past it. Asectionwith anaria-labelon all three pagesmax-width: 480pxits own measure, narrower than the 620px a column of prose takes. A sheet asks one question, and a narrower column makes that question look like one question.textblock.statusa molecule. The Text block at its status scope, measured here at 20px on the heading and 14px on the sentence, carrying both. This file gives it nothing, and the two rules that once did are named below. It was.stateuntil step 6.grid.roomyan organism. The Grid in its roomy form, one column narrow and two wide. It was.doorsuntil the fold at step 6. The only thing this file writes about it is 16px of margin underneath, and none at all when the door row is the last block in the sheet.doora molecule. The Door, with its own edge, its own hover and its own focus ring.quietan atom. The Quiet line, which is the way out. It is a line and not a button, deliberately: leaving is not one of the choicesbox-shadowreads--shadow, and this is its ONLY consumer in the product. It appears past a 760 container and nowhere else> :last-child { margin-bottom: 0 }the sheet's foot. Whatever ends the sheet brings a bottom margin from its own file for the block that would follow it, and in a sheet nothing follows it. Without this the card was 24px deeper at the foot than at the head
The foot was the defect that opened this page, and it was arithmetic rather than taste. Measured here at a 1280 viewport before 2026-08-12: the add-source sheet ran 384px tall, its last block ended at 335, and the gap under it was 49px where the card's padding is 24 and its rule is 1. The --space-24 the Quiet line carries was sitting on top of the --space-24 of padding. It is drawn twice, once per form: below 760 the sheet has no padding at all, so the same margin hung 24px under the sheet's own box and landed on the screen's foot instead.
Variants and sizes
One axis and it is the container, not the content. The same markup renders two ways at the same breakpoint the whole shell turns at, which is deliberate: the sheet becomes a card at exactly the width where the screen behind it becomes a layout with a rail, so it stops being the screen and starts being an object on it.
| Form Set by the CONTAINER and never by the content. Nothing in the markup chooses between these two, and no screen may opt out of either: all three sheets take both forms |
What changes | Why |
|---|---|---|
Full width .sheet, under container 760 |
no fill no edge no radius no shadow no padding |
On a phone the sheet IS the screen, so a card drawn around it would be a card drawn around the whole viewport: the screen's own 16px outside it and the card's 24px inside, two frames around one content |
Delete everything Tendd holds for you?This deletes your 14 subscriptions, both of your sources, and your account. It cannot be undone, and we keep no copy.
| ||
Centred card .sheet, from container 760 |
--bg-surface --line-container --radius --shadow --space-24 |
Past 760 the screen behind it has a rail and a wide content pane, and a 480px column alone in that pane reads as a layout accident. The card is what makes it read as a decision placed on the page |
| A modal over a dimmed page | - | Forbidden with a reason rather than missing. There is no overlay, no backdrop, no position: fixed, no <dialog> and no z-index on this component anywhere in the product. Each of these three is a screen a person navigated to and can navigate back from, with a named way out inside it. A modal would trap the one audience this product is built for on the one screen where the stakes are highest |
The stand shows it unbounded, and that is what lets the component set its own width. The stage measure is none here, so the 480px cap the sheet declares for itself is what you see: it is the only component in the system that carries its own measure rather than taking the screen's. Both specimens above are the real width, at both forms.
When to use it
A dialog sheet is for a decision that deserves a page. Three exist and they are the three moments where a person is being asked to choose rather than to look: "Add a source", which is D2's two equal doors offered again after the first time; "Cancelling can be made deliberately hard, and it is not your fault", which arrives when a cancel did not work; and "Delete everything Tendd holds for you?", which is the one irreversible act in the product.
Every one of the three carries a named way out that is not one of the choices. "Not now", "Remind me later", "Keep my data". That is not politeness, it is the consequence of drawing a dialog as a page: with no backdrop there is nothing to click beside, so the escape has to be inside the sheet and it has to be a control a person can see. It is also why voice/docs/microcopy.md gives each sheet its own exit line rather than reusing "Cancel", a word that means the opposite thing on the cancel screens.
The delete sheet is the sharpest test of the language and it passes without a red pixel. The destructive action is an outline button and the safe one is filled, so the accent marks the calm door rather than the loud one, and the sheet itself is paper on paper. Nothing about the frame says danger; the sentence says it, in words, once: "It cannot be undone, and we keep no copy."
The rule, and the anti-rule
Cancelling can be made deliberately hard, and it is not your fault
Netflix is still active. Here is what else to try, and none of it costs you the progress you have made.
One question, one column, and a way out beside the answer. Nothing behind it is dimmed and nothing is trapped, because there is nothing behind it: the sheet is the screen. Three of these in the product and each one is a route a person can leave by going back.
The steps above are free and always will be. Tendd Pro adds screenshots for each one and a direct link that skips the retention screens.
An offer that sits underneath something a person came for is the Panel and its gate, not a sheet. The test is whether the screen exists to ask: a sheet exists to ask and has nothing else on it, and a panel is a block inside a screen that is doing something else. Turning a gate into a sheet would put the paywall in front of the free thing, which is the one place D3 says it must never sit.
Add a source
The same two ways in, and you can have both. There is no limit on how many you add.
480px, at every width, and the card that appears past 760 is drawn around that same column rather than around a wider one. The doors inside still answer the container and go two-up, which is the grid's rule and not the sheet's.
Trends are part of Tendd Pro
Your list, your total and your alerts stay free and uncapped.
A Card, live, and it does not lift: a hairline and a background change and nothing else. The Flat Paper Rule, and this component is its single exception in the whole system. select.css records the same rule from the inside, which is why a floating select menu has no shadow either: "the one elevation in the system belongs to the dialog sheet alone". The moment two elevations exist inside one screen the eye starts ranking them, and ranking is the work this product takes away from the person.
Limits
U10, composition. One sheet at a time. A sheet over a sheet has no way back that a person can see. Three screens carry one and none carries two. Full table with sources and how to check: Usage rules.
States
No states, and the reason is one line: a sheet is not interactive. It is a section with an aria-label on all three occurrences, with no role, no href and no tabindex, and kit.css declared no pointer response on it. There is also nothing to dismiss: with no backdrop there is nothing to click beside, so each sheet's own way out is a control inside it, and those are Buttons and Quiet lines with their own states on their own pages. Faking a state here with a class the product does not carry would document something the product cannot do.
The one thing that looks like a state and is not is the card form past a 760 container. It is a second form, not a response: nothing a person does causes it, and it is in the matrix above with the query that fires it.
The technical half
| Reads | Which token | Growing from |
|---|---|---|
| the card surface, past 760 | --bg-surface | --paper, and --paper-dark in the dark theme |
| its edge, past 760 | --line-container | --hairline. Decorative at 1.23:1 light and 1.33:1 dark: nothing about a sheet has to be found by touch, and what a person presses is the button inside |
| its corner | --radius | primitive, straight: a corner has nothing to override |
| its lift | --shadow | primitive, straight, and this file is its only consumer in the product. Two layers, both mixed from the palette's own dark rather than from neutral black, so the lift stays inside the cool world |
| padding and margin | --space-0, --space-16, --space-24 | primitive, straight |
| the foot it takes from its last block | --space-0 | primitive, straight, and the only property this file sets on a hosted component. A margin is position and position is the host's, so the paint stays with quiet-line.css, action-row.css and grid.css. The door row has to be named a second time at 0-5-0, because .app .sheet > .grid.roomy is 0-4-0 and would otherwise hand its 16px back to a sheet that ends in its doors |
| no ink at all | none | by decision. Every word inside a sheet belongs to a Text block, a Pair list, a Button or a Quiet line |
The stacking order this component does not join. The brief for this round expected a z-index here and there is none: grepped across kit.css and wireframes/_wf.css, .sheet carries no z-index, no position and no overlay, and never has. The product's whole stacking order is three literals and all three are elsewhere: 1 on the currency prefix inside a field (a grid item, text-input.css), 5 on the app bar, 6 on the tab bar, with the pair deliberate so the bottom chrome wins where the two meet. The system has no z-index scale and --z-appbar and --z-tabbar were both proposed at stage 07 and deferred to stage 08, which created neither. This component adds nothing to that list, which is the useful answer: the question at step 9 is whether three literals in two files need a scale at all, and the round that was expected to add two added none.
DESIGN.md and the code disagree about the shadow and the code wins. The design doc calls it "the mobile app frame only" and says its job is to lift the whole mobile frame off the canvas. The stylesheet has shipped it on exactly one selector since stage 07, .app .sheet, and only inside the 760 container: it is a wide-screen shadow and there is no mobile frame anywhere in the CSS. Same finding as the subscription row's three sizes and the tab bar's label, same resolution, and the row belongs in docs/tokens-audit.md.
The foot, measured on both sides of the change and at both ends of the range. The four specimens on this page, at a 1280 viewport: 475, 320, 182 and 338 pixels tall, with 49, 41, 16 and 16 under the last block. After: 451, 304, 166 and 322, with 25, 25, 0 and 0. The two that end at 25 are the card form, which is 24 of padding and a 1px rule; the two that end at 0 stand inside a column narrower than 760 and take the narrow form, where the sheet has no padding of its own. At 360 all four take the narrow form and all four go to 0: 488, 270, 303 and 401 become 464, 254, 287 and 385. Nothing else on the page moved at either width.
Two rules were dropped a stage ago and are not brought back. wireframes/_wf.css lines 880 and 881 give .sheet h1 and .sheet p a size, a line height and an ink. kit.css refused both when the colour layer was built and said why at the line: all three sheets put their heading inside a State message, and the grey rules for those slots were silently restyling it. That is the defect this whole round exists to remove, found once by hand on three pages. It is not reintroduced here.
Lives in design/system/components/dialog-sheet.css. Stands on 3 grey pages, none of them coloured yet: wireframes/connections-add-source.html, wireframes/cancel-guide-blocked.html, wireframes/data-privacy-delete-confirm.html. It is the only organism in the system whose wide form has never been rendered in Petrol and Paper on a product screen, so every figure on this page was measured here, on the specimens above.
<section class="sheet" aria-label="Add a source">
<div class="textblock status">
<h1>Add a source</h1>
<p>The same two ways in, and you can have both. There is no limit on how many you add.</p>
</div>
<div class="grid roomy">
<a class="door" href="connect-bank.html">...</a>
<a class="door" href="add-subscription.html">...</a>
</div>
<p class="quiet"><a href="connections.html">Not now</a></p>
</section>
The 480 has only rendered past the tablet point since 2026-08-17, and this is the seventh element the shell's blanket unbound. This file has declared max-width: 480px since stage 08, with the reason on the rule: a sheet asks one question, and a column narrower than the flow's 620 makes that question look like one question. It never rendered. app-shell.css released every direct child of the screen at 0-2-0 from a file imported later, so the sheet ran the full flow column - 588px at 1440 on cancel guide, blocked, add a source and the delete confirmation. Stage 12 put the blanket inside :where() and this rule now wins by simply existing. Nothing moves on a phone, where 480 is wider than the screen; the three pages above are the whole of the change, 49 boxes.