Alert
.alert is a block of prose the reader is meant to stop and read. It is a
flex row with a .75rem gap, a
three-pixel border-inline-start that carries the tone, and a fill that
is a tint of the same colour. The start border is logical, so it moves to the right
edge under dir="rtl" without a second rule.
When to use it
Use an alert when the message is a sentence and it is about the page — a warning before a destructive action, an explanation of why a form is empty, the result of something the reader just did. It sits in the flow of the page and stays there.
An alert is not a notification. If the message is transient, arrives from the
server, or should stack with others, that is .toast. The difference is
whether the message is part of the page or an event that happened to it.
This project has no backups
Nightly backups start once a payment method is on file.
<div class="alert alert-warn">
<div>
<p class="alert-title">This project has no backups</p>
<p class="alert-body">Nightly backups start once a payment method is on file.</p>
</div>
</div>
<div class="alert alert-warn">
<div>
<p class="alert-title">This project has no backups</p>
<p class="alert-body">Nightly backups start once a payment method is on file.</p>
</div>
</div>
Tones
Five looks. Each sets the fill, the text colour and
border-inline-start-color, and sets the rest of the border to
transparent so the tone reads as one coloured edge rather than a box in
a colour.
Neutral
No tone. A note that carries no urgency at all.
Neutral
The border on the starting edge carries the tone.
<div class="alert">
<div>
<p class="alert-title">Neutral</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
<div class="alert">
<div>
<p class="alert-title">Neutral</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
Info
Something the reader should know before continuing.
Info
The border on the starting edge carries the tone.
<div class="alert alert-info">
<div>
<p class="alert-title">Info</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
<div class="alert alert-info">
<div>
<p class="alert-title">Info</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
Good
Confirmation that something worked.
Good
The border on the starting edge carries the tone.
<div class="alert alert-good">
<div>
<p class="alert-title">Good</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
<div class="alert alert-good">
<div>
<p class="alert-title">Good</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
Warn
A consequence the reader should understand first.
Warn
The border on the starting edge carries the tone.
<div class="alert alert-warn">
<div>
<p class="alert-title">Warn</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
<div class="alert alert-warn">
<div>
<p class="alert-title">Warn</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
Bad
Something failed, or is about to.
Bad
The border on the starting edge carries the tone.
<div class="alert alert-bad">
<div>
<p class="alert-title">Bad</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
<div class="alert alert-bad">
<div>
<p class="alert-title">Bad</p>
<p class="alert-body">The border on the starting edge carries the tone.</p>
</div>
</div>
With an icon
.alert > .icon gets flex: 0 0 auto so the icon never
shrinks, and margin-block-start: 1px so it sits on the title's optical
baseline rather than the line box's top. Nothing else is needed — the icon is a
direct child and the flex row does the rest.
Payment failed
The card ending 4242 was declined. Try another card.
<div class="alert alert-bad">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg>
<div>
<p class="alert-title">Payment failed</p>
<p class="alert-body">The card ending 4242 was declined. Try another card.</p>
</div>
</div>
<div class="alert alert-bad">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg>
<div>
<p class="alert-title">Payment failed</p>
<p class="alert-body">The card ending 4242 was declined. Try another card.</p>
</div>
</div>
The icon is decorative and carries aria-hidden="true". It repeats what
the tone and the title already say; a screen reader should not read "warning
triangle, Payment failed".
Title and body
.alert-title is a weight change and two pixels of bottom margin — not a
heading. .alert-body is
.8125rem and muted, with
min-inline-size: 0 so a long unbroken string inside it cannot push the
alert wider than its container.
.alert :is(.alert-body, p) resets the colour back to
inherit at 90% opacity. That is why the body text inside a tone stays
in the tone's colour family rather than reverting to the page's muted grey, which
would look like a mistake on a coloured fill.
Scheduled maintenance
Exports will be unavailable on Sunday between 02:00 and 04:00 UTC. Everything else keeps working.
<div class="alert alert-info">
<div>
<p class="alert-title">Scheduled maintenance</p>
<p class="alert-body">Exports will be unavailable on Sunday between 02:00 and 04:00 UTC. Everything else keeps working.</p>
</div>
</div>
<div class="alert alert-info">
<div>
<p class="alert-title">Scheduled maintenance</p>
<p class="alert-body">Exports will be unavailable on Sunday between 02:00 and 04:00 UTC. Everything else keeps working.</p>
</div>
</div>
Both parts are optional. An alert with one line of text and no
.alert-title is perfectly normal and is what most alerts should be.
Classes
Generated from src/07-components.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.alert |
deck.components |
A block of prose the reader is meant to stop and read. The tone is a three-pixel border-inline-start, so it moves to the right edge under dir="rtl" with no second rule. Deck sets no ARIA role: role="alert" on a message that is present at page load interrupts a screen reader before it has read anything. | 07-components.css:146 |
.alert-bad |
deck.components |
Something failed, or is about to. | 07-components.css:169 |
.alert-body |
deck.components |
The explanatory line. min-inline-size: 0 so a long unbroken string cannot push the alert wider than its container. | 07-components.css:160 |
.alert-good |
deck.components |
Confirmation that something worked. | 07-components.css:165 |
.alert-info |
deck.components |
Something the reader should know before continuing. | 07-components.css:163 |
.alert-title |
deck.components |
The first line of an alert. A weight change, not a heading — if it belongs in the document outline, put this class on a real h2-h4. | 07-components.css:157 |
.alert-warn |
deck.components |
A consequence the reader should understand first. | 07-components.css:167 |
Tokens
Each tone uses the 100 step of a status ramp for its fill, the
500 step for the edge, and the 700 step for text.
| Token | Live | Declared value |
|---|---|---|
--surface-2 |
light-dark(var(--ink-25), var(--ink-900)) |
|
--line |
light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral))) |
|
--ink-400 |
oklch(70.8% calc(var(--chroma-neutral) * 2.6) var(--hue-neutral)) |
|
--r-md |
12px |
|
--space-4 |
1rem |
|
--text-sm |
.8125rem |
|
--brand-soft |
light-dark(var(--brand-50), oklch(26% .05 var(--hue-brand))) |
|
--brand-soft-text |
light-dark(var(--brand-800), var(--brand-200)) |
|
--brand-500 |
oklch(60.8% var(--chroma-brand) var(--hue-brand)) |
|
--good-100 |
oklch(94% calc(var(--chroma-status) * .3) var(--hue-good)) |
|
--good-500 |
oklch(62% var(--chroma-status) var(--hue-good)) |
|
--good-700 |
oklch(44% var(--chroma-status) var(--hue-good)) |
|
--warn-100 |
oklch(95% calc(var(--chroma-status) * .32) var(--hue-warn)) |
|
--warn-500 |
oklch(76% var(--chroma-status) var(--hue-warn)) |
|
--warn-700 |
oklch(50% var(--chroma-status) var(--hue-warn)) |
|
--bad-100 |
oklch(94.5% calc(var(--chroma-status) * .3) var(--hue-bad)) |
|
--bad-500 |
oklch(58% calc(var(--chroma-status) * 1.25) var(--hue-bad)) |
|
--bad-700 |
oklch(44% calc(var(--chroma-status) * 1.1) var(--hue-bad)) |
Accessibility
-
Deck sets no ARIA role, and that is deliberate. An alert that is
part of the page when it loads should have no role at all — a
role="alert"on page load interrupts a screen reader before it has read anything else. -
Add
role="alert"only when it appears in response to something. A validation summary that shows up after a failed submit should interrupt. Add the role at the moment the element is inserted, or the announcement will not fire. -
For a message that should wait its turn, use
role="status"— polite rather than assertive. "Draft saved" belongs here; "Payment failed" belongs inrole="alert". -
The tone is not announced. Nothing about
.alert-badreaches a screen reader. The word "failed" has to be in the text, or the message is silent about its own severity. -
Colour contrast. The
100/700pairing clears 4.5:1 in both themes. The three-pixel edge is decoration on top of that, not the only cue. -
The title is not a heading.
.alert-titleis a paragraph in a heavier weight. If the alert needs to appear in the document outline, use a real<h2>–<h4>and put.alert-titleon it. -
Dismissible alerts need a real button. Deck ships no close
button for
.alert. If you add one, it needsaria-labeland a 44px target —.btn-icon.btn-ghost.btn-smgives both.
Right to left
The tone edge is border-inline-start, so it moves to the right under
dir="rtl" with no extra stylesheet. This is the clearest small example
of why Deck is written in logical properties: the same rule that draws a left edge
in English draws a right edge in Arabic, and there is no second file to keep in
step.
فشل الدفع
تم رفض البطاقة المنتهية بـ ٤٢٤٢.
<div dir="rtl" class="alert alert-bad">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg>
<div>
<p class="alert-title">فشل الدفع</p>
<p class="alert-body">تم رفض البطاقة المنتهية بـ ٤٢٤٢.</p>
</div>
</div>
<div dir="rtl" class="alert alert-bad">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg>
<div>
<p class="alert-title">فشل الدفع</p>
<p class="alert-body">تم رفض البطاقة المنتهية بـ ٤٢٤٢.</p>
</div>
</div>
Reduced motion
An alert has no transition and no animation of its own, so
prefers-reduced-motion changes nothing about it. If you animate one in,
do it in @media (prefers-reduced-motion: no-preference) so the
animation is never declared for a reader who has asked for less of it, rather than
declared and then cancelled.
Printing
src/99-print.css gives alerts the same treatment as cards and panels: a
#bbb hairline, no radius, a white background, and
break-inside: avoid. The tone fill is dropped, so a printed alert reads
as a bordered note rather than a coloured band — the words have to carry it, which
is the same reason the tone is never the only signal on screen.
Overriding it
No local custom properties; the tones set their colours directly. Override in
app.components.
@layer app.components {
.alert {
border-inline-start-width: 0;
border-radius: var(--r-lg);
padding: var(--space-5);
}
}
A tone of your own is three declarations — background,
color, border-inline-start-color — plus
border-color: transparent for the other three edges.
When not to use it
-
.alertversus.toast. An alert is part of the page and stays. A toast appears, stacks, and leaves. If the reader can scroll away and the message should not follow them, it is an alert; if it reports something that just finished, it is a toast. -
Not for field validation. Use
.errorbeneath the field and.is-invalidon the input. An alert at the top of a form that says "3 fields have errors" is a summary, and it needs the per-field messages as well, not instead. -
Not for a confirmation the reader must answer. That is
.modal, which takes focus and blocks. An alert can be scrolled past. -
Not for a permanent explanation. An alert that is on the page
every time is not an alert; readers stop seeing it within a week. Use
.dx-note-style prose, a.helpline, or say it in the copy. -
Not for one word. "Paid" is a
.badge. The alert's padding and border are sized for a sentence, and a one-word alert is a very large box around very little.