AlertBanner
A message that appears at the top of a screen because something needs your attention now, saying what has happened and what to do about it.
Preview
alert-banner · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/alert-banner
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { AlertBanner } from "@/components/ui/alert-banner"<AlertBanner
status="attention"
heading="Your example measurement is outside the range your clinic set"
detectedAt="2026-03-14T09:40:00+00:00"
actions={[
{ label: "Contact your clinic", href: "/clinic" },
{ label: "See the reading and the range", href: "/results/example" },
]}
>
Contact your clinic before your next appointment. This reading is outside the range they
asked us to tell you about.
</AlertBanner>When to use it
Use it when
- Something has changed since the reader last looked, and they need to know before carrying on.
- A reading has crossed a threshold the product owns, with a specific action attached.
- A condition affects the whole screen: stale data, a disconnected device, an unfinished sync.
Do not use it when
- Information the reader can take or leave, where nothing changes if they do not read it.Use
calloutinstead. - The status belongs to one result, not the screen.Use
status-pillinstead. - The content is an instruction with steps and timing, not an announcement.Use
care-cardinstead. - The reader must respond before anything else can happen.Use
dialoginstead. - Someone has disclosed thoughts of self-harm or suicide, which is not the status axis. See Crisis and self-harm.Use
care-cardinstead.
Clinical meaning
Asserts. That the product has decided this screen needs the reader's attention at one
of four levels, with something specific to do at attention and urgent. The component
compares nothing: status is assigned from a range the product owns.
Never read as. A clinical judgement about the reader, an instruction from a person, or a substitute for emergency services. A finding routed stop and call for help now leaves the banner. See Emergency and escalation.
Colour axis. Status only. There is no category prop, so a banner never carries
category colour. Urgency is never carried by motion either, and the component ships no
transition. See Motion in health UI.
Thresholds. None. No staleness default and no number in the component. The level's
word comes from the composed StatusPill, never from heading.
Budget. At most one urgent banner and two banners per screen, per
Alarm fatigue. The component cannot count across a screen.
No live region mounts at steady or watch, a named departure from
Clinical status semantics rule 6.
Anatomy
AlertBannerThe root, carrying data-status and the level's surface, ink and line roles.Controlled bystatusAlertBanner.AnnouncementThe live region around the heading and body only. Its role follows the level.AlertBanner.HeadingA real h2 to h6: the level's word, then the caller's subject.Controlled byheadingLevelStatusPillThe level's word and glyph, composed as the heading's first child.AlertBanner.BodyOne or two sentences, second person. Longer is a CareCard.Controlled bychildrenAlertBanner.ActionsAt most two, each a composed Link or Button. The first takes the emphasis.Controlled byactionsAlertBanner.DismissA quiet Button, last in the DOM. Rendered only with dismissible and onAcknowledge.Controlled bydismissible
Examples
Two is the ceiling
Two banners, the more serious first, because a reader meets them in DOM order. A third teaches the reader to scroll past the second.
alert-banner-two-is-the-ceiling · base base · style base-lyraOpen under the product themeAn urgent banner is acknowledged, not dismissed
dismissible renders nothing without onAcknowledge, and at urgent that callback is the
record. One action, because the route is same-day. Removing the banner drops focus to
<body>, which the product must recover.
alert-banner-acknowledging-an-urgent-banner · base base · style base-lyraOpen under the product themesteady is for de-escalation
The one honest use of the quietest level: withdrawing an alert the product raised earlier.
No actions, because only attention and urgent require one.
alert-banner-steady-is-for-de-escalation · base base · style base-lyraOpen under the product themeAccessibility
Audited against WCAG 2.2 AA in a source pass and a rendered pass. This is an author-run audit, not an independent review, and clinical review is still pending. The rendered pass was clean, with no axe violation and no target-size or focus-visible gap. The source pass found nothing to change.
Confirmed: colour never carries meaning alone, because the word and glyph come from the
composed StatusPill and shape and position separate the levels. The
live region wraps the heading and body only, role="alert" at urgent and
aria-live="polite" with aria-atomic at attention. Focus is never moved and every
control clears the 44pt floor.
Two residuals. At steady and watch the component mounts no live region, so a
de-escalation banner inserted after a condition resolves is not announced to a reader
already on the page. That is a deliberate departure from
Clinical status semantics rule 6 on
alarm-fatigue grounds, a doctrine call left for a human reviewer. Escalating in place also
relies on the caller passing key={status} to remount, which the component cannot
enforce.
Still ungated: any real screen reader, forced-colors, and the root's missing accessible
name.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves through the actions, then dismiss | The banner itself is not a tab stop. |
| Shift+Tab | The same in reverse | |
| Enter | Follows a link action or activates a button | href renders an anchor, onSelect a real button. |
| Space | Activates a button action or the dismiss | On a link action Space scrolls the page. |
| Escape | Nothing | The dismiss control is the only way out. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| attention ink on attention surface | light | 89.4 | 10.35:1 | Pass |
| attention ink on attention surface | dark | -81.1 | 12.54:1 | Pass |
| attention line on attention surface | light | 70.0 | 4.90:1 | Pass |
| attention line on attention surface | dark | -65.5 | 9.54:1 | Pass |
| attention accent on attention surface | light | 65.1 | 4.18:1 | Pass |
| attention accent on attention surface | dark | -49.4 | 6.80:1 | Pass |
| attention ink on the attention band | light | 22.4 | 2.48:1 | Below floor |
| attention ink on the attention band | dark | -31.0 | 1.84:1 | Below floor |
| attention accent on the page | light | 67.8 | 4.35:1 | Pass |
| attention accent on the page | dark | -50.5 | 7.47:1 | Pass |
| attention ink on the page | light | 92.2 | 10.77:1 | Pass |
| attention ink on the page | dark | -82.3 | 13.76:1 | Pass |
| steady ink on steady surface | light | 88.3 | 9.62:1 | Pass |
| steady ink on steady surface | dark | -82.9 | 12.56:1 | Pass |
| steady line on steady surface | light | 67.5 | 4.45:1 | Pass |
| steady line on steady surface | dark | -68.9 | 9.91:1 | Pass |
| steady accent on steady surface | light | 56.4 | 3.21:1 | Pass |
| steady accent on steady surface | dark | -53.2 | 7.25:1 | Pass |
| steady ink on the steady band | light | 30.0 | 3.00:1 | Below floor |
| steady ink on the steady band | dark | -28.8 | 1.73:1 | Below floor |
| steady accent on the page | light | 58.6 | 3.31:1 | Pass |
| steady accent on the page | dark | -54.5 | 8.19:1 | Pass |
| steady ink on the page | light | 90.6 | 9.94:1 | Pass |
| steady ink on the page | dark | -84.2 | 14.20:1 | Pass |
| unknown ink on unknown surface | light | 89.5 | 9.98:1 | Pass |
| unknown ink on unknown surface | dark | -82.2 | 12.56:1 | Pass |
| unknown line on unknown surface | light | 69.2 | 4.64:1 | Pass |
| unknown line on unknown surface | dark | -67.2 | 9.72:1 | Pass |
| unknown accent on unknown surface | light | 43.1 | 2.29:1 | Below floor |
| unknown accent on unknown surface | dark | -51.3 | 7.05:1 | Pass |
| unknown accent on the page | light | 45.6 | 2.37:1 | Below floor |
| unknown accent on the page | dark | -52.3 | 7.84:1 | Pass |
| unknown ink on the page | light | 92.0 | 10.34:1 | Pass |
| unknown ink on the page | dark | -83.1 | 13.97:1 | Pass |
| urgent ink on urgent surface | light | 88.2 | 10.58:1 | Pass |
| urgent ink on urgent surface | dark | -80.3 | 12.52:1 | Pass |
| urgent line on urgent surface | light | 70.0 | 5.09:1 | Pass |
| urgent line on urgent surface | dark | -64.8 | 9.52:1 | Pass |
| urgent accent on urgent surface | light | 72.3 | 5.58:1 | Pass |
| urgent accent on urgent surface | dark | -48.0 | 6.63:1 | Pass |
| urgent ink on the urgent band | light | 13.9 | 1.90:1 | Below floor |
| urgent ink on the urgent band | dark | -31.5 | 1.89:1 | Below floor |
| urgent accent on the page | light | 75.3 | 5.84:1 | Pass |
| urgent accent on the page | dark | -49.5 | 7.24:1 | Pass |
| urgent ink on the page | light | 91.2 | 11.06:1 | Pass |
| urgent ink on the page | dark | -81.8 | 13.66:1 | Pass |
| watch ink on watch surface | light | 89.4 | 10.02:1 | Pass |
| watch ink on watch surface | dark | -82.1 | 12.58:1 | Pass |
| watch line on watch surface | light | 69.5 | 4.72:1 | Pass |
| watch line on watch surface | dark | -66.8 | 9.68:1 | Pass |
| watch accent on watch surface | light | 61.7 | 3.72:1 | Pass |
| watch accent on watch surface | dark | -50.7 | 6.95:1 | Pass |
| watch ink on the watch band | light | 25.7 | 2.70:1 | Below floor |
| watch ink on the watch band | dark | -30.6 | 1.81:1 | Below floor |
| watch accent on the page | light | 64.2 | 3.85:1 | Pass |
| watch accent on the page | dark | -51.7 | 7.71:1 | Pass |
| watch ink on the page | light | 91.9 | 10.38:1 | Pass |
| watch ink on the page | dark | -83.1 | 13.95:1 | Pass |
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| attention surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| attention surface on the card material | dark | 0.0 | 1.00:1 | Below floor |
| steady surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| steady surface on the card material | dark | 0.0 | 1.03:1 | Below floor |
| urgent surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| urgent surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| watch surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| watch surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| the focus ring on the attention surface | light | 67.3 | 4.44:1 | Pass |
| the focus ring on the attention surface | dark | -45.9 | 6.30:1 | Pass |
| attention ink on the neutral hover fill | light | 88.6 | 10.23:1 | Pass |
| attention ink on the neutral hover fill | dark | -79.3 | 10.53:1 | Pass |
| the focus ring on the steady surface | light | 67.8 | 4.47:1 | Pass |
| the focus ring on the steady surface | dark | -45.8 | 6.12:1 | Pass |
| steady ink on the neutral hover fill | light | 87.0 | 9.43:1 | Pass |
| steady ink on the neutral hover fill | dark | -81.3 | 10.86:1 | Pass |
| the focus ring on the urgent surface | light | 67.0 | 4.42:1 | Pass |
| the focus ring on the urgent surface | dark | -45.5 | 6.34:1 | Pass |
| urgent ink on the neutral hover fill | light | 87.7 | 10.50:1 | Pass |
| urgent ink on the neutral hover fill | dark | -78.9 | 10.46:1 | Pass |
| the focus ring on the watch surface | light | 67.6 | 4.46:1 | Pass |
| the focus ring on the watch surface | dark | -46.0 | 6.24:1 | Pass |
| watch ink on the neutral hover fill | light | 88.3 | 9.86:1 | Pass |
| watch ink on the neutral hover fill | dark | -80.1 | 10.67:1 | Pass |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the root, always | alert-banner |
| data-slot | On inner parts | alert-banner-announcement, -heading, -heading-text, -body, -actions, -action, -dismiss |
| data-status | On the root, always | steady | watch | attention | urgent |
| data-detected-at | On the root, when detectedAt is non-empty | The ISO 8601 instant, verbatim. Absent otherwise. |
API reference
Prop
Type
Generated from AlertBannerProps in registry/bases/base/alert-banner.tsx.
A status outside the four levels renders nothing and reports OPSIN-0021 (OPSIN-0011 for
unknown). heading names the subject, because the component renders the level's word.
headingLevel outside 2 to 6 falls back to h2. A third action renders and is reported.
dismissible needs onAcknowledge at every level. now, locale and timeZone render
nothing. AlertAction is also exported:
export interface AlertAction {
label: string
href?: string
onSelect?: () => void
recommended?: boolean
}Related
- Callout carries no clinical level and interrupts nothing.
- CareCard is the instruction itself, with steps and timing.
- StatusPill is the same vocabulary on one item, not the screen.
- Toast is transient confirmation. Nothing clinical belongs on a timer.
- Dialog is for when the reader cannot continue until they respond.
GoalRing
A ring that shows how far a reading has come towards a goal the product set, paired with a text twin that states every figure, and rewarding no streak.
CareCard
A card that tells you what to do and when. It carries one instruction at one of three levels of urgency, with the urgency stated in the heading, not implied.