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.

Title and body, one tone

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.

The icon is a direct child, not inside the text wrapper

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.

The body inherits the tone rather than reverting to grey

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.

Classes, generated from the stylesheet source
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.

Design tokens this component reads
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 in role="alert".
  • The tone is not announced. Nothing about .alert-bad reaches 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/700 pairing 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-title is a paragraph in a heavier weight. If the alert needs to appear in the document outline, use a real <h2>–<h4> and put .alert-title on it.
  • Dismissible alerts need a real button. Deck ships no close button for .alert. If you add one, it needs aria-label and a 44px target — .btn-icon.btn-ghost.btn-sm gives 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.

The tone edge follows the writing direction

فشل الدفع

تم رفض البطاقة المنتهية بـ ٤٢٤٢.

<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

  • .alert versus .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 .error beneath the field and .is-invalid on 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 .help line, 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.