Alert Banner
A persistent, priority-ranked message that tells a clinician what has changed, how urgent it is, and what to do next — without taking over the screen.
AcuteLine is a fictional SaMD used as the worked example across this system. It ingests 12-lead and continuous ECG plus high-sensitivity troponin results and flags patients with suspected acute coronary syndrome (ACS) for clinician review. It is decision support: it never diagnoses, never triggers therapy, and every output is confirmed by a qualified clinician. All screens, copy and patient data are fabricated.
Overview
An Alert Banner occupies a fixed region of the layout and stays there until the underlying condition resolves or a clinician acts on it. It is the workhorse for communicating change in a SaMD: a new detection, a degraded input, a failed analysis, a completed action.
In AcuteLine it carries the moment that matters most — the transition from "ECG received" to "this patient may be having a STEMI." That transition has to survive a noisy resuscitation bay, a clinician looking at a different monitor, and a screen three metres away.
When to use
- A detection or measurement crosses a threshold that requires a clinician to look or act.
- An input the algorithm depends on has degraded — lead off, signal noise, missing lab result.
- Analysis has failed, been skipped, or produced an indeterminate result.
- An action the clinician started has completed and the outcome must be visible on screen.
When not to use
- To confirm trivial interactions. Saving a filter needs no banner.
- To block a workflow. Use
<Dialog>. A banner must never be the only thing between a clinician and an urgent task. - For per-field validation. Errors belonging to one input belong under it.
- As a toast, at Critical or Urgent. Never use
<Sonner>for a clinical alarm. An alarm you can miss by looking away is a hazard, not a convenience.
Design principles
Unambiguous
Priority, patient and finding must be readable in one glance, from across the room, by someone mid-task. If a clinician has to read it twice, the banner has failed.
Proportionate
Visual force matches clinical consequence — never engagement value. Every banner promoted above its true priority spends attention the next real alarm will need.
Actionable
State the next step and put the control to take it inside the banner. An alert that reports a problem but leaves the clinician hunting has moved work, not removed it.
Anatomy
Priority levels
Four priority levels plus one confirmation level. The first three map onto IEC 60601-1-8 alarm priorities and inherit that standard's mandated hues. These are not brand decisions and must not be re-themed — see Colour.
| Level | IEC | Means | AcuteLine example | Token |
|---|---|---|---|---|
| critical | High | Immediate response required | STEMI pattern detected | --alarm-critical |
| urgent | Medium | Prompt response required | hs-troponin rising on 0/1 h algorithm | --alarm-urgent |
| advisory | Low | Awareness; can wait | Lead V3 signal quality degraded | --alarm-advisory |
| info | Not an alarm | State change, no response | Analysis complete — no acute pattern | --alarm-info |
| confirmation | Not an alarm | A clinician action succeeded | Cath lab notified at 14:32 | --alarm-ok |
Variants
docked
Full-bleed, solid fill, pinned beneath the patient header. The only variant permitted for
critical. Use when the alert concerns the whole record in view.
inline
Scoped to a card or panel: surface drops to --card and priority moves to a
4 px rail. Use when the alert concerns one region — a single lead, one lab panel, one chart.
The lower chroma is deliberate; an inline alert must not out-shout a docked one on the same screen.
compact
Single line, no description or provenance. Permitted only inside dense repeating structures such as a triage worklist, and only when the full alert is one click away.
States & behaviour
| State | Appearance | Rules |
|---|---|---|
| active | Full priority chroma | Entry 220 ms ease-out. Never re-animate on re-render. |
| acknowledged (timed) | Chroma drops to rail; countdown shown | Returns to active automatically when the timer lapses if the condition persists. |
| acknowledged (indefinite) | Chroma drops to rail | Persists until resolved. Writes user and timestamp to the audit log. |
| audio-paused | Visually unchanged; separate audio indicator | Pausing audio must never alter or hide the visual signal. |
| latched | Full chroma retained | Condition resolved but was never seen. Stays until acknowledged. |
| resolved | Removed, or replaced by a confirmation | Critical alerts are never removed silently — always leave an event-log trace. |
Stacking and coalescence
- Show at most three docked banners. Beyond three, collapse the lowest priorities into a summary row — never the highest.
- Sort strictly by priority, then most-recent-first within a priority.
- An arriving
criticalrenders above everything and enters the focus order, but must not steal keyboard focus mid-typing. - Coalesce only alerts identical in patient, finding and priority; show count and latest time.
Writing the message
Alert copy is a safety surface. Write for a reader who is interrupted, tired, reading at distance, and about to make a decision.
- Title ≤ 60 characters. Front-load the finding, not the software.
- Name the anatomy or measurement. "Anterior leads V2–V4", "ST elevation 3.1 mm" — specificity is what makes an alert verifiable.
- Say what to do in one imperative sentence, referring to local protocol rather than prescribing therapy.
- Never claim a diagnosis. Use "pattern detected", "cannot be excluded", "consistent with". The software detects; the clinician diagnoses.
- Avoid negation stacks. "Not non-diagnostic" is unreadable at 3 a.m.
- Budget 35 % for translation. Test German and Finnish before release.
Names the finding, the territory and the measurement, then hands the decision back to the clinician.
Claims a diagnosis the software is not cleared to make, leads with the product name, and gives no territory, measurement or action.
More do's and don'ts
Provenance on every algorithm-derived alert: who, when, which model, how sure.
No patient, no time, no model version. Correct on the wrong record and nobody could tell.
A recoverable input problem, announced so the clinician knows the interpretation is limited.
Silent about the degraded lead. The result looks as trustworthy as any other.
Accessibility
- Never encode priority in colour alone. Every level pairs its hue with a distinct glyph and a distinct word in the title. Verify in greyscale.
- Contrast. Every fill meets AA against its paired foreground. Amber, cyan and grey carry black text — white on amber measures 1.68:1 and is a hard failure. Do not "fix" them to white for visual consistency.
- Live regions.
criticalandurgentrenderrole="alert"; the rest renderrole="status". Never make five banners assertive at once. - Focus. Arriving banners must not steal focus. Provide a documented shortcut that jumps to the highest-priority banner.
- Motion. Under
prefers-reduced-motion, render with no transition — never with no alert. - Zoom and reflow. Operable at 200 % zoom and a 320 px viewport with no horizontal scrolling; actions wrap below the text rather than truncating.
- No flashing. Escalate by priority, audio and position — never by flicker.
Measured contrast
| Fill | Foreground | Ratio | Result |
|---|---|---|---|
--alarm-critical |
White | 5.64:1 | AA |
--alarm-urgent |
Black | 12.49:1 | AAA |
--alarm-advisory |
Black | 8.78:1 | AAA |
--alarm-info |
Black | 10.98:1 | AAA |
--alarm-ok |
White | 6.07:1 | AA |
Critical and confirmation sit in the AA band, not AAA. Reaching 7:1 would darken the red past the point where it reads as an alarm hue, which IEC conformance depends on. The trade is deliberate and belongs in your accessibility conformance report.
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — The correct priority is recognised at a glance and the finding is acted on for the right patient. Measured by correct-priority recognition and by wrong-patient actions per 1 000 alerts.
- Efficiency — Time to acknowledge, by priority, and the attention each alert consumes. Both degrade as alarm burden rises — see Acknowledge & escalate.
- Satisfaction — Whether a clinician believes an alert at this priority is worth stopping for. Every alert promoted above its true priority spends that belief, and it is not quickly rebuilt.
Clinical safety notes
These behaviours mitigate identified hazards and should be traced in your risk file (ISO 14971). Changing them is a design change with safety consequence, not a styling decision.
- No auto-dismiss above advisory. Mitigates missed detection on an unattended display.
- Provenance is mandatory. Patient, time, model version and confidence on every algorithm-derived alert. Mitigates wrong-patient action and use of a superseded model.
- Confidence is shown, never hidden behind a hover. Mitigates automation bias.
- Negative results state their own limits. Mitigates false reassurance.
- Primary action opens evidence, never an irreversible step such as notifying the cath lab. Mitigates acting on an unreviewed algorithmic output.
- Degraded input is announced, not silently tolerated. Mitigates silent failure.
- Acknowledgement is attributable — user and timestamp to the audit log.
Usage do's and don'ts
Match the level to the consequence. A recoverable input problem is advisory.
Inflating an electrode problem to critical trains clinicians to discount red — the definition of alarm fatigue.
State the limits of a negative result in the banner itself.
Reads as an all-clear the software cannot give, and invites premature discharge.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/alert-banner.json
components/ui/alert-banner.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertBannerVariants = cva(
"relative flex w-full items-start gap-3 rounded-md border px-4 py-3 shadow-sm",
{
variants: {
priority: {
critical: "bg-alarm-critical text-alarm-critical-foreground border-black/20",
urgent: "bg-alarm-urgent text-alarm-urgent-foreground border-black/20",
advisory: "bg-alarm-advisory text-alarm-advisory-foreground border-black/20",
info: "bg-alarm-info text-alarm-info-foreground border-black/15",
confirmation: "bg-alarm-ok text-alarm-ok-foreground border-black/20",
},
variant: {
docked: "",
inline: "bg-card text-card-foreground border-border border-l-4 shadow-none",
compact: "items-center gap-2 px-3 py-1.5",
},
state: {
active: "",
acknowledged: "bg-card text-card-foreground border-border border-l-4 shadow-none",
},
},
defaultVariants: { priority: "info", variant: "docked", state: "active" },
}
)
// Safety invariants — enforced here so no product team has to remember them.
const ALARM_PRIORITIES = ["critical", "urgent"] as const
export interface AlertBannerProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertBannerVariants> {
/** Required when the alert is patient-specific. */
subjectId?: string
/** Required for algorithm-derived alerts. */
modelVersion?: string
/** 0–1. Rendered to two decimals; never abbreviated to a word. */
confidence?: number
observedAt?: string
dismissible?: boolean
}
const AlertBanner = React.forwardRef<HTMLDivElement, AlertBannerProps>(
({ className, priority = "info", variant, state, dismissible, ...props }, ref) => {
const isAlarm = ALARM_PRIORITIES.includes(priority as never)
if (isAlarm && dismissible && process.env.NODE_ENV !== "production") {
console.warn("[AlertBanner] `dismissible` ignored for alarm priorities.")
}
return (
<div
ref={ref}
role={isAlarm ? "alert" : "status"}
aria-live={isAlarm ? "assertive" : "polite"}
data-priority={priority}
className={cn(alertBannerVariants({ priority, variant, state }), className)}
{...props}
/>
)
}
)
AlertBanner.displayName = "AlertBanner"
export { AlertBanner, alertBannerVariants }
Usage
<AlertBanner
priority="critical"
variant="docked"
subjectId="44812907"
observedAt="2026-08-13T14:26:38Z"
modelVersion="4.2"
confidence={0.94}
>
<AlertBannerTitle>STEMI pattern detected — anterior leads V2–V4</AlertBannerTitle>
<AlertBannerDescription>
ST elevation 3.1 mm. Confirm ECG and activate local chest-pain pathway.
</AlertBannerDescription>
<AlertBannerActions>
<Button size="sm" variant="secondary">Open ECG</Button>
<AcknowledgeButton reassertAfter="PT10M" />
</AlertBannerActions>
</AlertBanner>
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
priority |
critical | urgent | advisory | info | confirmation |
info | Sets colour, glyph and ARIA role together. Not independently overridable. |
variant | docked | inline | compact |
docked | compact is rejected at critical outside a worklist. |
state | active | acknowledged | active |
Acknowledged drops chroma but never text. |
subjectId | string | — | Required for patient-specific alerts. |
modelVersion | string | — | Required for algorithm-derived alerts. |
confidence | number 0–1 | — | Rendered to two decimals; never abbreviated to a word such as "high". |
dismissible | boolean | false |
Ignored at critical and urgent; warns in development. |
Events
onAcknowledge—{ id, userId, at, mode: 'timed' | 'indefinite' }onReassert— fired when a timed acknowledgement lapses and the condition persists.onAction—{ id, action: 'primary' | 'secondary' }