Skip to content

Alert

A banner that reports something the reader did not ask for. It owns no state: it never hides itself, never runs a timer, and the only JavaScript in the file is the branch that decides whether a screen reader should interrupt.

  • Stable
  • React 19
  • Zero JS
  • 4 variants
A software update is available.
The backup was restored.
A software update is available.
Code
<Alert color="info">A software update is available.</Alert>
<Alert color="error">The server refused the connection.</Alert>
<div class="wayra-alert" role="status" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-color="info"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">A software update is available.</div></div>
<div class="wayra-alert" role="alert" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-color="error" role="alert"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">The server refused the connection.</div></div>

The colour is the meaning, and it is the only prop most alerts need. It defaults to neutral rather than primary, because “primary” says nothing about a piece of feedback — an alert with no status is just a notice.

soft — the role washed over the surface
filled — the role as the background
outline — the role as the edge
dash — the same edge, dashed
Code
<Alert color="info">soft</Alert>
<Alert variant="filled" color="info">filled</Alert>
<Alert variant="outline" color="info">outline</Alert>
<Alert variant="dash" color="info">dash</Alert>
<div class="wayra-alert" role="status" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-color="info"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">soft — the role washed over the surface</div></div>
<div class="wayra-alert" role="status" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-variant="filled" data-color="info"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">filled — the role as the background</div></div>
<div class="wayra-alert" role="status" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-variant="outline" data-color="info"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">outline — the role as the edge</div></div>
<div class="wayra-alert" role="status" data-variant="soft" data-color="neutral" data-orientation="horizontal" data-variant="dash" data-color="info"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">dash — the same edge, dashed</div></div>

soft is the default, which is a deliberate break from Button and Badge: those default to filled because they are small, and a small saturated shape reads as one object. An alert spans the container, and a full-width saturated bar shouts. soft mixes 8% of the role into base-100 — the same wash Button spends on its ghost hover — and leaves the text alone.

Battery service mode
Peak performance is limited until it is checked at an authorised centre.
Code
<Alert
color="warning"
icon={<BatteryIcon />}
title="Battery service mode"
actions={<Button variant="outline" color="warning" size="sm">Details</Button>}
>
Peak performance is limited until it is checked at an authorised centre.
</Alert>
<div class="wayra-alert" role="status" data-variant="soft" data-color="warning" data-orientation="horizontal"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body"><div class="wayra-alert-title">Battery service mode</div><div class="wayra-alert-description">Peak performance is limited until it is checked at an authorised centre.</div></div><div class="wayra-alert-actions"><button type="button" class="wayra-button" data-variant="outline" data-color="warning" data-size="sm">Details</button></div></div>

Three optional slots, each with a class of its own so the same markup can be written by hand. Without a title the children are the whole body; with one they become the description and drop a step in size. The library ships no icons — icon takes whatever you hand it, and an <svg> inside is sized to 1.25em so it follows the reader’s text size.

The alert has no onClose: visibility is the consumer's state.

Code
{visible && (
<Alert
color="error"
actions={
<Button variant="ghost" color="error" size="sm" shape="circle"
aria-label="Dismiss" onClick={() => setVisible(false)}>
✕
</Button>
}
>
The server refused the connection.
</Alert>
)}
<div class="wayra-alert" role="alert" data-variant="soft" data-color="error" data-orientation="horizontal"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body">The server refused the connection.</div><div class="wayra-alert-actions"><button type="button" class="wayra-button" data-variant="ghost" data-color="error" data-size="sm" data-shape="circle" aria-label="Dismiss">✕</button></div></div>

There is no close button in the component and no onClose prop. A dismissable alert is a <Button> in the actions slot plus a piece of state you already own — which keeps the alert free of a second button implementation, and keeps you in charge of what “closed” means (hidden, collapsed, remembered).

A new accessory was detected
Set it up now, or later from Settings.
Code
<Alert
orientation="vertical"
color="info"
icon={<AccessoryIcon />}
title="A new accessory was detected"
actions={<>
<Button variant="outline" color="info" size="sm">Later</Button>
<Button color="info" size="sm">Set up</Button>
</>}
>
Set it up now, or later from Settings.
</Alert>
<div class="wayra-alert" role="status" data-variant="soft" data-color="info" data-orientation="vertical"><span class="wayra-alert-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.2"/><path d="M12 11v6M12 7.6v.01"/></svg></span><div class="wayra-alert-body"><div class="wayra-alert-title">A new accessory was detected</div><div class="wayra-alert-description">Set it up now, or later from Settings.</div></div><div class="wayra-alert-actions"><button type="button" class="wayra-button" data-variant="outline" data-color="info" data-size="sm">Later</button><button type="button" class="wayra-button" data-color="info" data-size="sm">Set up</button></div></div>

vertical puts every part on its own row. It is one flex-direction, not a second layout, which is why it costs two declarations and cannot drift from the horizontal case.

The class and the data-* attributes are the API. The stylesheet is written against them, so hand-authored markup gets the same banner with no React anywhere — that is what every html panel on this page is.

Hand-written
No component, same paint.
Code
<Alert color="success" title="Hand-written">
No component, same paint.
</Alert>
<div class="wayra-alert" role="status" data-variant="soft" data-color="success" data-orientation="horizontal">
<div class="wayra-alert-body">
<div class="wayra-alert-title">Hand-written</div>
<div class="wayra-alert-description">No component, same paint.</div>
</div>
</div>

The part classes are equally public: .wayra-alert-icon, .wayra-alert-body, .wayra-alert-title, .wayra-alert-description and .wayra-alert-actions. unstyled drops all of them together with the root class and leaves the data-* attributes in place.

AlertProps extends ComponentProps<"div"> minus color, role and title. The first two are managed (see below); the third is the banner heading, which costs the native tooltip attribute of the same name.

Prop Type Default Description
variant "soft" | "filled" | "outline" | "dash" "soft" How the role is spent: wash, fill or edge.
color WayraColor "neutral" Semantic role, and the meaning of the alert.
orientation "horizontal" | "vertical" "horizontal" Whether the parts share a row or stack.
icon ReactNode — Leading graphic, drawn in the role colour.
title ReactNode — Heading; turns the children into the description.
actions ReactNode — Trailing controls, aligned to the far edge.
live "assertive" | "polite" | "off" derived How assistive technology announces it.
unstyled boolean false Drops the classes, keeps the data-*.

There is no size, no shape and no width: an alert fills the container it sits in and has one step, like Card.

  • The announcement role is derived, not typed. warning and error render role="alert" and interrupt; every other colour renders role="status" and waits for a pause. live overrides that, and live="off" writes no role at all — the right choice for a banner that is part of the page rather than news.
  • role is not a prop. It is computed from live, so the markup and the announcement policy cannot disagree.
  • Inserted is not the same as changed. Most screen readers announce a role="alert" as soon as it is inserted — the APG alert pattern says so — which is why {error && <Alert color="error">…</Alert>} works. A polite alert is a role="status", and a status region is announced when its content changes, not when it appears: MDN’s advice is to establish the region first and update it after. So a polite message that must be heard keeps its Alert mounted and swaps the children, and the component keeps the same region node across that swap. The APG also warns that an alert already on the page when it loads is not announced, which is one more reason a banner that is part of the page takes live="off".
  • The role colour never paints running text. Measured on the default theme: a title in its own role over the soft wash reads 4.07–4.33:1, under the 4.5:1 the colour contract promises against a clean base-100. The role now paints the icon and the edge, and all text is base-content.
  • Nothing here is focusable. Any control inside actions is; give an icon-only dismiss button an aria-label.