Alert
Alert — the message that appears in the page, in response to something.
Examples
We could not save your changes
The server rejected the request. Nothing was lost — try again.
<div class="alert alert-danger" role="alert">
<p class="alert-title">We could not save your changes</p>
<p class="alert-text">The server rejected the request. Nothing was lost — try again.</p>
</div>There is no separate callout component any more: it was this one with a different name and no live role, and a review put the two pages side by side and could not tell them apart.
Inside prose, write an alert with no role at all and add .alert-note so the title sits at the size of the copy around it — role="alert" and role="status" announce changes, and on a paragraph that has been there since load they either say nothing or interrupt a reader mid-sentence. A message that is genuinely transient is a toast.
Your session expires in one minute
Save anything unsaved, or sign in again to continue.
<div class="alert alert-warning" role="alert">
<p class="alert-title">Your session expires in one minute</p>
<p class="alert-text">Save anything unsaved, or sign in again to continue.</p>
<div class="alert-actions">
<button class="btn btn-sm btn-primary" type="button">Stay signed in</button>
<button class="btn btn-sm btn-outline" type="button">Sign out</button>
</div>
</div>The four variants are .alert-info, .alert-success, .alert-warning and .alert-danger, and each changes the glyph as well as the tint — colour alone would be a WCAG 1.4.1 failure the moment two sat next to each other.
Buttons in .alert-actions are .btn-sm: an alert is not a dialog and its actions are not the page's main ones.
Your changes are saved.
<div class="alert alert-success" role="status">
<p class="alert-text">Your changes are saved.</p>
<button class="btn btn-sm btn-quiet btn-icon alert-close" type="button">
<svg class="icon" aria-hidden="true"><use href="#icon-x"></use></svg>
<span class="sr-only">Dismiss this message</span>
</button>
</div>role="status" waits for a pause; role="alert" interrupts.
The role must be on an element that existed before the message did — one injected complete with its role frequently announces nothing at all. And when the alert disappears, focus has to go somewhere deliberate: that is the consumer’s call, and the docblock says so.
Your trial ends on Friday
Nothing is deleted; the workspace becomes read-only until you choose a plan.
Invoice sent
A copy is on its way to accounts@acme.example.
Two invoices are overdue
Access is suspended after thirty days.
The import failed on row 412
Nothing was written. Fix the row and try again.
<div class="alert alert-info">
<p class="alert-title">Your trial ends on Friday</p>
<p class="alert-text">Nothing is deleted; the workspace becomes read-only until you choose a plan.</p>
</div>
<div class="alert alert-success" role="status">
<p class="alert-title">Invoice sent</p>
<p class="alert-text">A copy is on its way to accounts@acme.example.</p>
</div>
<div class="alert alert-warning" role="alert">
<p class="alert-title">Two invoices are overdue</p>
<p class="alert-text">Access is suspended after thirty days.</p>
</div>
<div class="alert alert-danger" role="alert">
<p class="alert-title">The import failed on row 412</p>
<p class="alert-text">Nothing was written. Fix the row and try again.</p>
</div>The glyph changes with the tint, never instead of it: colour alone would be a WCAG 1.4.1 failure the moment two of these sat next to each other, and the four marks differ in shape as well as in hue.
The glyph is a pseudo-element, so it cannot be pasted without aria-hidden and cannot drift out of step with the variant class — the two are one declaration. The bar down the inline start is a border-inline-start, so it cannot be clipped by the radius or covered by the content, and it moves to the right edge in Arabic with no second rule.
No mark, no rail
For a message whose meaning is carried entirely by its words. The room reserved outside the box goes with them.
A note in prose: .alert-note puts the
title at the size of the copy around it, and an alert used this way carries
no role at all — a live role on a paragraph that has been on the page
since load either says nothing or interrupts a reader mid-sentence.
<div class="alert alert-plain">
<p class="alert-title">No mark, no rail</p>
<p class="alert-text">For a message whose meaning is carried entirely by its words. The room reserved outside the box goes with them.</p>
</div>
<div class="alert alert-info alert-banner">
<p class="alert-title">Scheduled maintenance on Sunday, 02:00–04:00 UTC</p>
<p class="alert-text">Exports queued during the window run afterwards.</p>
</div>
<p class="alert alert-warning alert-note">A note in prose: <code>.alert-note</code> puts the
title at the size of the copy around it, and an alert used this way carries
<strong>no role at all</strong> — a live role on a paragraph that has been on the page
since load either says nothing or interrupts a reader mid-sentence.</p>.alert-banner is the full-width strip at the top of a page or a panel: no radius and no inline edges, so it meets its container on both sides, and the coloured bar moves from the inline start to the bottom edge, where it reads as a rule under the strip.
A bar down the side of a full-width band would end at two square corners and look like a stub. .alert-plain drops the glyph — the icon column collapses to nothing rather than the pseudo-element being removed, so there is one layout to reason about and no stray column gap left behind.
Tokens 26
Level 2, declared on .alert itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --alert-actions-gap | var(--space-4) |
| --alert-text-color | var(--color-on-surface) |
| --alert-text-font-size | var(--font-size-sm) |
| --alert-title-color | var(--color-on-surface) |
| --alert-title-font-size | var(--font-size-base) |
| --alert-title-font-weight | var(--font-weight-semibold) |
| --alert-accent | var(--color-border-strong) |
| --alert-bg | var(--color-surface-sunken) |
| --alert-border-color | var(--color-border) |
| --alert-border-width | var(--border-width) |
| --alert-color | var(--color-on-surface) |
| --alert-accent-size | 3px |
| --alert-padding-block | var(--space-4) |
| --alert-padding-inline | var(--space-4) |
| --alert-radius | var(--radius-box) |
| --alert-close-reserve | 2.5rem |
| --alert-close-size | 2rem |
| --alert-icon | var(--alert-icon-info) |
| --alert-icon-color | var(--alert-accent) |
| --alert-icon-size | 1.25rem |
| --alert-icon-check | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m8.5 12.5 2.5 2.5 4.5-5'/%3E%3C/svg%3E") |
| --alert-icon-danger | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='m15 9-6 6'/%3E%3Cpath d='m9 9 6 6'/%3E%3C/svg%3E") |
| --alert-icon-info | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E") |
| --alert-icon-warning | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3'/%3E%3Cpath d='M12 9v4'/%3E%3Cpath d='M12 17h.01'/%3E%3C/svg%3E") |
| --alert-flow | var(--space-2) |
| --alert-gap | var(--space-3) |
Variants and states
Variants
.alert-danger.alert-info.alert-note.alert-success.alert-warning
Inside it
.alert-close.alert-actions.alert-banner.alert-plain.alert-text.alert-title
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
data-legacy
Before you ship it
What you have to do 4 requirements
- Pick the role from the urgency, not from the variant's colour.
.alert-dangerand.alert-warningtakerole="alert", which interrupts..alert-successand.alert-infotakerole="status", which waits. An admonition that was on the page before the reader arrived takes no role at all: a live region on static prose either says nothing or interrupts someone mid-sentence for a paragraph that has always been there. - Ship the live region empty with the page and insert the message into it. A
<div role="alert">inserted into the document already holding its text is announced in no engine. - Put it where the thing it is about happened — at the top of the form that failed, above the list that changed — not in a corner of the layout.
- Give
.alert-closean accessible name. It is a<button>, its glyph isaria-hidden, and the name belongs on the button.
src/css/components/alert.css · npx mostlycss add alert