Feedback
Alert
An inline message about the page or a task, in one of four tones, with an optional action.
- Built on
ARIA live regions- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.5 kB
- Dependencies
- React
Card expires in 3 days
Update the Visa ending 4242 to keep your Pro plan.
Try its options
Loading its options.
Code
import { Alert } from "@/components/equip/alert";
export default function Demo() {
return (
<Alert tone="warn" title="Card expires in 3 days" style={{ width: 216 }}>
Update the Visa ending 4242 to keep your Pro plan.
</Alert>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:216px" class="eq-alert" data-tone="warn">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M8 2.25l6.25 11H1.75z" />
<path d="M8 6.75v3M8 11.5v.01" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Card expires in 3 days</p>
<div class="eq-alert-text">Update the Visa ending 4242 to keep your Pro plan.</div>
</div>
</div>Examples
Tones
Each tone has its own icon shape, so the difference survives greyscale and colour blindness.
Maintenance on 18 Nov, 02:00 UTC
The dashboard is read-only for about 20 minutes.
Domain verified
app.example.com now serves production traffic.
You have used 92% of build minutes
New builds queue once you reach 3,000.
Deploy failed
next build exited with code 1 in src/app/layout.tsx.
Code
import { Alert } from "@/components/equip/alert";
export default function Example() {
return (
<div style={{ display: "grid", gap: 8, maxWidth: 420 }}>
<Alert title="Maintenance on 18 Nov, 02:00 UTC">The dashboard is read-only for about 20 minutes.</Alert>
<Alert tone="ok" title="Domain verified">app.example.com now serves production traffic.</Alert>
<Alert tone="warn" title="You have used 92% of build minutes">New builds queue once you reach 3,000.</Alert>
<Alert tone="danger" announce={false} title="Deploy failed">
next build exited with code 1 in src/app/layout.tsx.
</Alert>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:420px">
<div class="eq-alert" data-tone="info">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" />
<path d="M8 7.25v3.5M8 5.25v.01" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Maintenance on 18 Nov, 02:00 UTC</p>
<div class="eq-alert-text">The dashboard is read-only for about 20 minutes.</div>
</div>
</div>
<div class="eq-alert" data-tone="ok">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" />
<path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Domain verified</p>
<div class="eq-alert-text">app.example.com now serves production traffic.</div>
</div>
</div>
<div class="eq-alert" data-tone="warn">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M8 2.25l6.25 11H1.75z" />
<path d="M8 6.75v3M8 11.5v.01" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">You have used 92% of build minutes</p>
<div class="eq-alert-text">New builds queue once you reach 3,000.</div>
</div>
</div>
<div class="eq-alert" data-tone="danger">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
<path d="M6 6l4 4M10 6l-4 4" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Deploy failed</p>
<div class="eq-alert-text">next build exited with code 1 in src/app/layout.tsx.</div>
</div>
</div>
</div>With an action
The action sits at the end of the row, after the text, so it is reached after the message is read.
Payment failed
We could not charge $480.00 for invoice INV-2041.
Code
import { Button } from "@/components/equip/button";
import { Alert } from "@/components/equip/alert";
export default function Example() {
return (
<Alert
tone="danger"
announce={false}
title="Payment failed"
action={
<Button size="s" variant="outline">
Update card
</Button>
}
style={{ maxWidth: 440 }}
>
We could not charge $480.00 for invoice INV-2041.
</Alert>
);
}HTMLworks in any framework, with the part's CSS
<div style="max-width:440px" class="eq-alert" data-tone="danger">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
<path d="M6 6l4 4M10 6l-4 4" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Payment failed</p>
<div class="eq-alert-text">We could not charge $480.00 for invoice INV-2041.</div>
</div>
<div class="eq-alert-action">
<button type="button" class="eq-button" data-variant="outline" data-size="s">
<span class="eq-button-label">Update card</span>
</button>
</div>
</div>Title or body alone
Changes saved to eu-west-1
Invites expire after 7 days. Resend from the Members page.
Code
import { Alert } from "@/components/equip/alert";
export default function Example() {
return (
<div style={{ display: "grid", gap: 8, maxWidth: 420 }}>
<Alert tone="ok" title="Changes saved to eu-west-1" />
<Alert>Invites expire after 7 days. Resend from the Members page.</Alert>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;max-width:420px">
<div class="eq-alert" data-tone="ok">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" />
<path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
</svg>
<div class="eq-alert-body">
<p class="eq-alert-title">Changes saved to eu-west-1</p>
</div>
</div>
<div class="eq-alert" data-tone="info">
<svg class="eq-alert-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6.25" />
<path d="M8 7.25v3.5M8 5.25v.01" />
</svg>
<div class="eq-alert-body">
<div class="eq-alert-text">Invites expire after 7 days. Resend from the Members page.</div>
</div>
</div>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
tone | "info" | "ok" | "warn" | "danger" | "info" | Sets the icon shape and the colour of the icon and border. |
title | ReactNode | none | One line that says what happened. |
action | ReactNode | none | Placed at the end of the row, usually a small Button. |
announce | boolean | tone === "danger" | Makes it a live region: role="alert" for danger, role="status" for every other tone. False sets no role. |
...rest | ComponentProps<"div"> | none | Every native div attribute except title and role, which the part owns. |
Anatomy
| root | .eq-alert | Tone is on data-tone. |
|---|---|---|
| icon | .eq-alert-icon | A different shape per tone. |
| body | .eq-alert-body | |
| title | .eq-alert-title | |
| text | .eq-alert-text | |
| action | .eq-alert-action |
Notes
- Renders on the server with no client JavaScript.
- A live region is announced when its content changes, not when it is already on the page at load; render it empty first, or mount it when the event happens.
- Only danger interrupts with role="alert". Set announce={false} for a danger message that is part of the page rather than news.
- The icon is hidden from assistive tech, so write a title that states the tone in words, such as "Deploy failed".
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/alert.tsx84 lines
import type { ComponentProps, ReactNode } from "react";
import "./alert.css";
type Tone = "info" | "ok" | "warn" | "danger";
export type AlertProps = Omit<ComponentProps<"div">, "title" | "role"> & {
/** Carried by the icon shape and a tone-coloured border, never by colour alone. */
tone?: Tone;
/** One line that says what happened. */
title?: ReactNode;
/** Usually a small Button, placed at the end. */
action?: ReactNode;
/**
* Makes the alert a live region so screen readers announce it when it appears:
* role="alert" for danger, role="status" for every other tone.
* Defaults to true for danger and false otherwise.
*/
announce?: boolean;
};
/* One shape per tone, 16px, drawn in the tone colour. */
const icons: Record<Tone, ReactNode> = {
info: (
<>
<circle cx="8" cy="8" r="6.25" />
<path d="M8 7.25v3.5M8 5.25v.01" />
</>
),
ok: (
<>
<circle cx="8" cy="8" r="6.25" />
<path d="M5.5 8.25l1.75 1.75 3.25-3.5" />
</>
),
warn: (
<>
<path d="M8 2.25l6.25 11H1.75z" />
<path d="M8 6.75v3M8 11.5v.01" />
</>
),
danger: (
<>
<path d="M5.4 1.75h5.2l3.65 3.65v5.2l-3.65 3.65H5.4l-3.65-3.65V5.4z" />
<path d="M6 6l4 4M10 6l-4 4" />
</>
),
};
export function Alert({
tone = "info",
title,
action,
announce = tone === "danger",
className,
children,
...rest
}: AlertProps) {
return (
<div
{...rest}
className={["eq-alert", className].filter(Boolean).join(" ")}
data-tone={tone}
role={announce ? (tone === "danger" ? "alert" : "status") : undefined}
>
<svg
className="eq-alert-icon"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{icons[tone]}
</svg>
<div className="eq-alert-body">
{title != null && <p className="eq-alert-title">{title}</p>}
{children != null && <div className="eq-alert-text">{children}</div>}
</div>
{action != null && <div className="eq-alert-action">{action}</div>}
</div>
);
}components/equip/alert.css67 lines
@layer equip {
.eq-alert {
--_tone: var(--eq-ink-2);
display: grid;
/* Two tracks; the action adds a third only when it is there, so no empty gutter. */
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: var(--eq-space-2) var(--eq-space-3);
padding: var(--eq-space-3);
border: 1px solid color-mix(in srgb, var(--_tone) 55%, transparent);
border-radius: var(--eq-radius-m);
background: var(--eq-tint);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-s) / 1.45 var(--eq-font);
}
.eq-alert[data-tone="ok"] {
--_tone: var(--eq-ok);
}
.eq-alert[data-tone="warn"] {
--_tone: var(--eq-warn);
}
.eq-alert[data-tone="danger"] {
--_tone: var(--eq-danger);
}
/* Nudged down so its centre sits on the first line of text. */
.eq-alert-icon {
width: 16px;
height: 16px;
margin-top: 1px;
color: var(--_tone);
}
.eq-alert-body {
display: grid;
gap: 2px;
min-width: 0;
}
.eq-alert-title {
margin: 0;
font-weight: var(--eq-weight-strong);
}
.eq-alert-text {
color: var(--eq-ink-2);
}
.eq-alert-text > :first-child {
margin-top: 0;
}
.eq-alert-text > :last-child {
margin-bottom: 0;
}
.eq-alert-action {
grid-column: 3;
display: flex;
gap: var(--eq-space-2);
align-self: center;
}
}