Skip to content
Primitiv home
Framework
Consumption mode

Alert

stableSource Figma

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

Density

Preview

Size
Tone
import { Alert } from "@/components/ui/alert";
<Alert tone="info" size="md" onDismiss={() => setShown(false)} title="Scheduled maintenance">  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

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

<Alert tone="danger" title="Payment failed">  Your card was declined.</Alert>

Props

Alert

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultFromDescription
children (required)ReactNodeheadlessThe alert's message. Always present — the required content.
dismissLabelstring"Dismiss"headlessaria-label for the dismiss button.
iconReactNodeheadlessOverrides the tone's default leading icon (info circle / success check / warning triangle / danger x-circle).
onDismiss() => voidheadlessCalled when the dismiss button is activated. The dismiss button only renders when this is provided; omit it for a non-dismissible alert.
titlestring & ReactElement<unknown, string | JSXElementConstructor<any>> | string & Iterable<ReactNode> | string & ReactPortal | string & Promise<AwaitedReactNode>headlessThe alert's heading, shown above the description. Omit for a single-message alert — the headless Alert's own simplest usage (<Alert>{error}</Alert>).
tone"info" | "success" | "warning" | "danger"infostyledSemantic colour; also selects the default leading icon.
size"xs" | "sm" | "md" | "lg" | "xl"mdstyledAlert size; data-density scales each size further.

Styling contract

--primitiv-alert-background--primitiv-alert-foreground--primitiv-alert-border-color--primitiv-alert-border-width--primitiv-alert-padding-inline--primitiv-alert-padding-block--primitiv-alert-gap--primitiv-alert-icon-size--primitiv-alert-icon-offset-top--primitiv-alert-dismiss-size--primitiv-alert-dismiss-inset-inline--primitiv-alert-dismiss-inset-block--primitiv-alert-radius--primitiv-alert-title-font-family--primitiv-alert-title-font-size--primitiv-alert-title-font-weight--primitiv-alert-title-line-height--primitiv-alert-description-font-family--primitiv-alert-description-font-size--primitiv-alert-description-font-weight--primitiv-alert-description-line-height--primitiv-alert-dismiss-hover-background--primitiv-alert-dismiss-active-background

Accessibility

  • 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

It interrupts (the headline)

Alert is an assertive live regionrole="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.

Density
import { Alert } from "@/components/ui/alert";
{/* Assertive: interrupts. Only for "you must know this now". */}<Alert tone="danger" title="Payment failed">  Your card was declined.</Alert>
{/* Calmer alternative — not this component. */}<div role="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.

Density
import { Alert } from "@/components/ui/alert";import { Stack } from "@/components/ui/stack";
<Stack gap="md">  <Alert tone="info" title="Scheduled maintenance">    The API will be read-only on Sunday from 02:00 UTC.  </Alert>  <Alert tone="success" title="Deploy complete">    Version 4.2.0 is live on production.  </Alert>  <Alert tone="warning" title="Quota almost used">    You have used 92% of this month's build minutes.  </Alert>  <Alert tone="danger" title="Payment failed">    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.

Density
import { Alert } from "@/components/ui/alert";import { Stack } from "@/components/ui/stack";
<Stack gap="md">  <Alert tone="success">    Changes saved.  </Alert>  <Alert tone="warning" title="Quota almost used">    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.

Density
import { useState } from "react";import { Alert } from "@/components/ui/alert";
const [shown, setShown] = useState(true);
{shown && (  <Alert tone="success" onDismiss={() => setShown(false)}>    Changes saved.  </Alert>)}

Sizes and density

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.

Density
import { Alert } from "@/components/ui/alert";import { Stack } from "@/components/ui/stack";
<div data-density="comfortable">  <Stack gap="md">    <Alert size="xs" title="XS">      Your card was declined.    </Alert>    <Alert size="sm" title="SM">      Your card was declined.    </Alert>    <Alert size="md" title="MD">      Your card was declined.    </Alert>    <Alert size="lg" title="LG">      Your card was declined.    </Alert>    <Alert size="xl" title="XL">      Your card was declined.    </Alert>  </Stack></div>