The interruption
Something stopped: a load failed, a link expired, a list is empty, a payment did not go through. The container says what happened, in one sentence a person can act on, and gives the way on directly under it. It is the most common composition in the product and the one most likely to be built badly, because every screen meets it at its worst moment.
Anatomy
Two required parts and one optional one, in this order and no other. Everything below is drawn by design/system/patterns/interruption.css plus the component files it arranges, through the same ../system/index.css a product screen loads, so this page cannot drift from the screens.
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.
You can come back to this from any Pro feature. Nothing about your subscriptions or your data changed.
.interruptionthe class goes on the CONTAINER that holds the pair, not on a wrapper of its own: a wrapper would be markup added to a product screen for the system's convenience, and it would not survive the dialog sheet.textblock.statusthe announcement, first. It carriesrole="status"andaria-live="polite"on every one of the pages it stands on, so the sentence reaches a screen reader when the state arrives rather than only on the way past.actionsthe way out, second, and it is not optional. One filled action and, where there is a second way, a plain one beside it.quietor.muted: the closing line, optional, and when it is there the container is also an action foot. A quiet line carries a real exit ("Do this later"); a muted line carries a consequence ("Nothing about your plan changed")marginthe only value the pattern owns: 24px between the announcement and whatever follows it. It was the text block's own bottom margin and it still is everywhere else; inside an interruption the block's margin is zeroed and the gap is declared once, on the composition
Variants
No visual variants, and that is the point of a pattern. What varies is the host it stands in and what stands between the announcement and the exit, and neither changes a value: the four hosts below render identically because the pattern reads the host's width and nothing else. The fourth arrived on 2026-08-13, from the first screen built after this page existed, and it cost no CSS at all: an axis read off the corpus that stands is a description of that corpus, and the next screen is allowed to extend it.
| Host The container the class goes on. All three are containers the product already had; the pattern adds none |
Selector | Where it stands |
|---|---|---|
| The screen, and the screen is now for this | main.screen.interruption | Eleven pages. The whole screen has stopped: the trends failed to load, the reveal has nothing to reveal, the sign-in link expired |
We could not load your trendsSomething on our side did not answer. Your subscriptions, your total and your alerts are not affected: this is only the history view. | ||
| One column of a detail | .col1.interruption | One page, subscription detail, could not load. The identity of the thing survives in the hero above and only the rest of the record failed, so the interruption takes the column and not the screen |
| A dialog sheet | .sheet.interruption | Three pages, all still grey: the cancel guide with no guide, the add-a-source sheet, the delete confirmation. No coloured page carries this host yet, so it is the one part of this pattern that the pixel comparison could not check. It reaches colour at stage 12 |
| The list column, added 2026-08-13 | .rows-col.interruption | One page, alerts, could not reach your alerts, and it is the first screen built after this pattern was written. The three hosts above all hold the announcement as a direct child of the screen or of a detail column; this screen puts its list column in between, because its grey original does. The class moves to whichever element actually holds the pair, which is what a child selector means. Measured on the page at both viewports: 24px with the class and 24px without it |
| A tone: error red, warning yellow | - | Forbidden with a reason rather than missing. A status block IS the whole container: there is nothing beside it to be told apart from, and a fill around a full-screen message is decoration rather than information. The product has one desaturated clay for a genuine error and it is spent on the sentence, never on a panel behind it. This is the calm rule of Concept at the composition level |
When to use it
Every flow in ia/docs/flows.md meets it. Flow A hits it when the bank connection is cancelled or fails, Flow B when the service list will not load, Flow C in the cancel guide's dead end, Flow D at an alert that leads to a screen with nothing on it, Flow E when the sign-in link has expired. That is the reason it earned a name before anything else did: it is not one screen's problem, it is the shape the product takes whenever it has to say no.
The rule that makes it calm is in the copy, and the composition protects it. The announcement says what happened and what is still fine, in that order, and the exit is a real one. Both come from voice/docs/voice.md: no fault, no exclamation, and a way out always present. A composition that puts the button above the sentence would invite the opposite text, which is why the order is part of the pattern rather than a habit.
It is not the same thing as an empty block. The empty block is a component for a zone inside a working screen, the way Home shows an empty list under a head that still works. The interruption is the container itself having stopped.
The rule, and the anti-rule
Nothing to reveal yet
Add at least one subscription and your list appears here. Even a partial list is saved, so you can come back any time.
Nothing else is on the screen, because nothing else can be. The announcement, the way on, and a real way past it. This is the pattern.
We could not refresh just now. Your figures are from this morning. Try again
You're paying for 14 subscriptions
Home when the refresh failed. Every figure is still there and still true, so the screen has not stopped and this is not an interruption: it is a wash block annotating a screen that works, with the recovery inside the sentence that explains it. Turning this into an interruption would hide fourteen subscriptions to report that one request was slow. Two more pages are this case and not the pattern: add-subscription-empty, where the manual form under the notice is the point of the screen, and cancel-guide-no-guide, where the guide beside it is still readable.
That link has expired
Links last one hour, so we can be sure it is you. Ask for a new one and it will be there in a moment.
A person taps and something happens. Every exit in this pattern is an edge a person takes.
Confirming with your bank
This usually takes a few seconds.
upgrade-processing and connect-bank-loading carry a status block and no way out at all, and neither takes this class. An edge the system takes is not a control: a button on a wait screen offers an action the product does not have, and on a charge in flight it offers the one thing that screen must not offer. That is rule U7 in the usage rules, and these two pages are its whole evidence.
Limits
U4, composition. One .textblock.status per screen, so one interruption per screen. Two announcements are two states. U6, composition. The way out is required and lives in the same container, except on cancel-guide-no-guide, where it is in the column beside it. U1, composition. One filled action in the row, whatever else the exit offers. Full table with sources: Usage rules.
Where it stands
Sixteen pages across twelve screens, counted on the grey product, which is the whole of it. Seven are in colour today and carry the class; the other nine are still grey and take it at the rollout. This list is the proof the pattern exists, so it is named page by page rather than summarised.
| Screen | Pages | Host | In colour |
|---|---|---|---|
| Add subscription | add-subscription-error | screen | yes |
| Guided reveal | guided-reveal-empty | screen | yes |
| History and trends | history-trends-empty, history-trends-error | screen | yes |
| Subscription detail | subscription-detail-error | .col1 | yes |
| Upgrade | upgrade-payment-failed | screen | yes |
| Connect bank | connect-bank-cancelled, connect-bank-empty, connect-bank-error | screen | grey, stage 12 |
| Sign in | sign-in-expired, sign-in-sent | screen | grey, stage 12 |
| Alerts | alerts-error | .rows-col | yes |
| Share snapshot | share-snapshot-error | screen | grey, stage 12 |
| Cancel guide | cancel-guide-blocked | .sheet | grey, stage 12 |
| Connections | connections-add-source | .sheet, exit is a grid of doors | grey, stage 12 |
| Data and privacy | data-privacy-delete-confirm | .sheet | grey, stage 12 |
The dialog sheet host is the one thing on this page that pixels have not checked, because no sheet exists in colour yet. It is said here rather than left to be noticed: three grey pages, none proved, and the rollout is where it gets proved.
The technical half
| Declares | Value | Where it came from |
|---|---|---|
| the gap under the announcement | var(--space-24) | the text block's own bottom margin, which stays in text-block.css for the other 27 text blocks in the product. Inside the pattern it is zeroed there and declared here, so the composition has one home |
| colour, type, radius, elevation | none | a pattern arranges components and reads tokens. A hex here would be a component nobody declared |
Lives in design/system/patterns/interruption.css, imported at the foot of design/system/index.css, after every component. Measured before and after the extraction at 360 and 1280 on all six coloured pages: the gap is 24px on both sides of the change, and no element moved. Under 900 the screen is normal flow and adjacent margins collapse, so 24 and 0 collapse the same way 0 and 24 do; at 900 and past it the screen is a flex column and both readings are a plain sum. The account is on the proof.
<main class="screen interruption act-foot">
<div class="textblock status" role="status" aria-live="polite">
<h1>That payment did not go through</h1>
<p>Your bank did not approve it, so nothing was charged.</p>
</div>
<div class="actions">
<a class="btn primary" href="upgrade.html">Try another payment method</a>
<a class="btn" href="history-trends-locked.html">Maybe later</a>
</div>
<p class="muted">You can come back to this from any Pro feature.</p>
</main>