An assertive banner for high-priority, time-sensitive messages — a tone-matched icon, an optional title, a description, and an optional dismiss button. Composes the headless Alert primitive (role="alert") for the message and the registry Button (ghost variant) for the dismiss affordance. Sized xs–xl; data-density scales each size further.
Playground
Preview
Scheduled maintenance
The API will be read-only on Sunday from 02:00 UTC.
import{Alert}from"@/components/ui/alert";<Alerttone="info"size="md"onDismiss={()=>setShown(false)}title="Scheduled maintenance"> The API will be read-only on Sunday from 02:00 UTC.</Alert>
import{Alert}from"@primitiv-ui/react";<AlertonDismiss={()=>setShown(false)}>{/* your own icon / title anatomy goes here */}<strong>Scheduled maintenance</strong> The API will be read-only on Sunday from 02:00 UTC.</Alert>
Density is set by a data-density ancestor — the Context system, not a Alert prop.
Installation
npx primitiv add alert
pnpm dlx primitiv add alert
yarn dlx primitiv add alert
bunx primitiv add alert
Import
import{Alert}from"@/components/ui/alert";
Copied into your project as .primitiv-alert — you own the file afterwards, so upgrades are opt-in.
Headless mode installs the npm package instead: @primitiv-ui/react
Anatomy
The widest gap between the two surfaces on the site, and it is not a rename. @primitiv-ui/react exports a single Alert that is a bare <div role="alert"> taking only children and asChild — it supplies the live region and nothing else. Every prop documented below (tone, title, icon, onDismiss, dismissLabel) belongs to the copied file, which builds the icon, the title, the description and the dismiss button around that region. Under Headless there is no tone to pass; you compose the anatomy yourself.
<Alerttone="danger"title="Payment failed"> Your card was declined.</Alert>
<Alert>{/* your own icon / title anatomy goes here */}<strong>Payment failed</strong> Your card was declined.</Alert>
Props
Generated from source — headless rows from each *Props type’s JSDoc, styled rows from contract.json. Never hand-maintained.
Alert
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
From
Description
children* (required)
ReactNode
—
headless
The alert's message. Always present — the required content.
dismissLabel
string
"Dismiss"
headless
aria-label for the dismiss button.
icon
ReactNode
—
headless
Overrides the tone's default leading icon (info circle / success
check / warning triangle / danger x-circle).
onDismiss
() => void
—
headless
Called when the dismiss button is activated. The dismiss button only
renders when this is provided; omit it for a non-dismissible alert.
Assertive by construction.role="alert" implies aria-live="assertive" and aria-atomic="true", so the alert interrupts a screen reader mid-sentence and is read as a whole. That is the right behaviour for an error the user must act on, and the wrong behaviour for standing page furniture. role is set internally and passing your own is ignored — if you want a polite region, this is not the component.
It has to arrive, or change, to be announced. A live region that is already on the page at load is not announced, so an Alert rendered with the initial HTML says nothing. Mount it in response to the event it describes. The headless primitive helps here by injecting its children after the region is in the DOM, which is the difference between a reliable announcement and a silent one.
Do not stack several alerts at once. Each one interrupts, so three appearing together produce three interruptions and the user hears fragments of all of them. Prefer one alert that states the situation.
tone is visual only — all four announce identically, and colour is not available to every reader. Put the severity in the words: "Payment failed" rather than a red alert reading "Card declined".
The dismiss button is a real Button instance with an accessible name, defaulting to "Dismiss". When a page can show more than one alert, give each a specific dismissLabel — several buttons all called "Dismiss" are indistinguishable in a screen reader's element list.
Dismissing is not the same as resolving. onDismiss fires and the caller unmounts; nothing announces that the alert has gone, so if the underlying problem persists, do not let a dismissal imply it is fixed.
A title is styled text, not a heading — it emits no <h*> and enters no document outline. That is deliberate for an interrupting region, and it is why the whole alert is announced atomically rather than as a heading followed by prose.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
It interrupts (the headline)
Alert is an assertive live region — role="alert", which implies aria-live="assertive" and aria-atomic="true". A screen reader abandons whatever it was saying and reads the alert immediately. That is right for a failed payment and wrong for a page banner about next Sunday's maintenance, so reach for it only when the message is genuinely time-sensitive and the user must know now. For anything calmer, render your own container with role="status" (polite), or no role at all if there is nothing to announce because the content was there when the page loaded. The headless primitive injects its children after the region is in the DOM, which is what makes the announcement fire at all — a live region that is added already-populated is frequently not announced.
Payment failed
Your card was declined. Try a different payment method.
import{Alert}from"@/components/ui/alert";{/* Assertive: interrupts. Only for "you must know this now". */}<Alerttone="danger"title="Payment failed"> Your card was declined.</Alert>{/* Calmer alternative — not this component. */}<divrole="status">Draft saved.</div>
import{Alert}from"@primitiv-ui/react";{/* Assertive: interrupts. Only for "you must know this now". */}<Alert>{/* your own icon / title anatomy goes here */}<strong>Payment failed</strong> Your card was declined.</Alert>{/* Calmer alternative — not this component. */}<divrole="status">Draft saved.</div>
Tones
Four tones, and each one also picks the leading icon — an info circle, a success check, a warning triangle, a danger x-circle — so tone is one decision rather than a colour plus a matching glyph. Override the glyph with icon when the default is wrong for the message; the tone's colour stays. Note that tone is purely visual: all four are the same assertive live region, so a success alert interrupts exactly as a danger one does. Colour is not the message either — the text has to carry it for anyone who cannot see the tone.
Scheduled maintenance
The API will be read-only on Sunday from 02:00 UTC.
Deploy complete
Version 4.2.0 is live on production.
Quota almost used
You have used 92% of this month's build minutes.
Payment failed
Your card was declined. Try a different payment method.
import{Alert}from"@/components/ui/alert";import{Stack}from"@/components/ui/stack";<Stackgap="md"><Alerttone="info"title="Scheduled maintenance"> The API will be read-only on Sunday from 02:00 UTC.</Alert><Alerttone="success"title="Deploy complete"> Version 4.2.0 is live on production.</Alert><Alerttone="warning"title="Quota almost used"> You have used 92% of this month's build minutes.</Alert><Alerttone="danger"title="Payment failed"> Your card was declined. Try a different payment method.</Alert></Stack>
import{Alert}from"@primitiv-ui/react";import{Stack}from"@/components/ui/stack";<Stackgap="md"><Alert>{/* your own icon / title anatomy goes here */}<strong>Scheduled maintenance</strong> The API will be read-only on Sunday from 02:00 UTC.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>Deploy complete</strong> Version 4.2.0 is live on production.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>Quota almost used</strong> You have used 92% of this month's build minutes.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>Payment failed</strong> Your card was declined. Try a different payment method.</Alert></Stack>
With and without a title
title is optional, and leaving it off is the headless primitive's own simplest shape — one line of message beside the icon. Use a title when the alert has both a summary and a detail worth separating; skip it when the whole message is one sentence, because a title that just restates the sentence reads as a stutter. children is always required: it is the message, not a slot you can leave empty.
Changes saved.
Quota almost used
You have used 92% of this month's build minutes.
import{Alert}from"@/components/ui/alert";import{Stack}from"@/components/ui/stack";<Stackgap="md"><Alerttone="success"> Changes saved.</Alert><Alerttone="warning"title="Quota almost used"> You have used 92% of this month's build minutes.</Alert></Stack>
import{Alert}from"@primitiv-ui/react";import{Stack}from"@/components/ui/stack";<Stackgap="md"><Alert> Changes saved.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>Quota almost used</strong> You have used 92% of this month's build minutes.</Alert></Stack>
Dismissing
onDismiss is what makes the dismiss button exist — there is no dismissible boolean, because a dismiss control with nothing listening is a button that does nothing. Two consequences worth planning for. It does not hide the alert for you: the callback is yours, and so is the state that unmounts it, which is what lets you persist the dismissal or animate the exit. And the button is a real Button (ghost variant) instance rather than a bare glyph, so it inherits the focus ring and hit area — matching the Modal close convention. Rename it with dismissLabel when "Dismiss" is not specific enough.
Five sizes, each rescaling again with the nearest data-density ancestor. Size moves the padding, the icon, the type ramp and the dismiss button together, so the alert stays proportionate rather than growing a large icon beside small text.
XS
Your card was declined. Try a different payment method.
SM
Your card was declined. Try a different payment method.
MD
Your card was declined. Try a different payment method.
LG
Your card was declined. Try a different payment method.
XL
Your card was declined. Try a different payment method.
import{Alert}from"@/components/ui/alert";import{Stack}from"@/components/ui/stack";<divdata-density="comfortable"><Stackgap="md"><Alertsize="xs"title="XS"> Your card was declined.</Alert><Alertsize="sm"title="SM"> Your card was declined.</Alert><Alertsize="md"title="MD"> Your card was declined.</Alert><Alertsize="lg"title="LG"> Your card was declined.</Alert><Alertsize="xl"title="XL"> Your card was declined.</Alert></Stack></div>
import{Alert}from"@primitiv-ui/react";import{Stack}from"@/components/ui/stack";<divdata-density="comfortable"><Stackgap="md"><Alert>{/* your own icon / title anatomy goes here */}<strong>XS</strong> Your card was declined.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>SM</strong> Your card was declined.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>MD</strong> Your card was declined.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>LG</strong> Your card was declined.</Alert><Alert>{/* your own icon / title anatomy goes here */}<strong>XL</strong> Your card was declined.</Alert></Stack></div>