Alert
An inline message about the state of the page or a form. It sits in the layout and stays until the condition clears.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
Alert is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { Alert } from "@dovetail-ds/react";
Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.
Guidelines
An inline message about the state of the page or a form. It sits in the layout and stays until the condition clears.
Use it when
- A form failed validation at the form level.
- The page is in a state the user should know about: trial ending, sync paused.
Don't use it when
- The message confirms an action the user just took. Use
Toast. - It relates to one field. Use that field's
errorprop. - It is permanent marketing copy. That is page content, not an alert.
Example
<Alert tone="danger" title="We couldn't save your changes">
Your session expired. Sign in again and retry.
</Alert>
Accessibility
danger renders role="alert" and interrupts; the other tones render role="status" and wait for a pause. Omit onDismiss when the user must act. A dismissible blocker is a dead end.
Content
Title says what happened. Body says what to do next. Never lead with an apology.
Props
The typed contract, from Alert.d.ts.
import * as React from "react";
/** Inline message about the state of the page or a form. Stays until the condition clears. */
export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
/** @default "info" */
tone?: "info" | "success" | "warning" | "danger";
title?: React.ReactNode;
/** Action slot, usually a ghost or secondary Button. */
action?: React.ReactNode;
/** Renders a dismiss button. Omit for messages the user must resolve. */
onDismiss?: () => void;
children?: React.ReactNode;
}
export declare function Alert(props: AlertProps): React.JSX.Element;
Tokens it reads
Every custom property Alert.jsx resolves, read from the source: 26 semantic, 1 primitive. Component tier first, because that is the one to reach for.
- semantic Shared roles. Override one and everything using that role moves with it.
- primitive Raw values. A component reading one directly is a deliberate exception.
| Token | Tier | Declared as |
|---|---|---|
--dt-border-danger | semantic | var(--dt-color-red-200) |
--dt-border-info | semantic | var(--dt-color-cyan-200) |
--dt-border-success | semantic | var(--dt-color-green-200) |
--dt-border-warning | semantic | var(--dt-color-amber-200) |
--dt-border-width-default | semantic | var(--dt-dim-hair) |
--dt-radius-container | semantic | var(--dt-radius-raw-16) |
--dt-size-icon-md | semantic | var(--dt-dim-5) |
--dt-space-inline-sm | semantic | var(--dt-dim-3) |
--dt-space-inset-md | semantic | var(--dt-dim-4) |
--dt-space-stack-2xs | semantic | var(--dt-dim-1) |
--dt-surface-danger-subtle | semantic | var(--dt-color-red-050) |
--dt-surface-info-subtle | semantic | var(--dt-color-cyan-050) |
--dt-surface-success-subtle | semantic | var(--dt-color-green-050) |
--dt-surface-warning-subtle | semantic | var(--dt-color-amber-050) |
--dt-text-body-sm-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-sm-line | semantic | var(--dt-line-height-sm) |
--dt-text-body-sm-size | semantic | var(--dt-font-size-sm) |
--dt-text-danger | semantic | var(--dt-color-red-800) |
--dt-text-info | semantic | var(--dt-color-cyan-900) |
--dt-text-label-md-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-md-line | semantic | var(--dt-line-height-sm) |
--dt-text-label-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-primary | semantic | var(--dt-color-neutral-900) |
--dt-text-secondary | semantic | var(--dt-color-neutral-600) |
--dt-text-success | semantic | var(--dt-color-green-800) |
--dt-text-warning | semantic | var(--dt-color-amber-900) |
--dt-font-weight-semibold | primitive | 600 |
Source
React only, no dependencies, every value a token.
import React from "react";
const TONES = {
info: { bg: "var(--dt-surface-info-subtle)", bd: "var(--dt-border-info)", fg: "var(--dt-text-info)", icon: "M12 16v-4 M12 8h.01", ring: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20" },
success: { bg: "var(--dt-surface-success-subtle)", bd: "var(--dt-border-success)", fg: "var(--dt-text-success)", icon: "m9 12 2 2 4-4", ring: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20" },
warning: { bg: "var(--dt-surface-warning-subtle)", bd: "var(--dt-border-warning)", fg: "var(--dt-text-warning)", icon: "M12 9v4 M12 17h.01", ring: "m10.3 3.6-8 14A2 2 0 0 0 4 20.5h16a2 2 0 0 0 1.7-2.9l-8-14a2 2 0 0 0-3.4 0" },
danger: { bg: "var(--dt-surface-danger-subtle)", bd: "var(--dt-border-danger)", fg: "var(--dt-text-danger)", icon: "M15 9l-6 6 M9 9l6 6", ring: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20" },
};
export function Alert({ tone = "info", title, action, onDismiss, children, style, ...rest }) {
const t = TONES[tone] || TONES.info;
return (
<div
role={tone === "danger" ? "alert" : "status"}
style={{
display: "flex", gap: "var(--dt-space-inline-sm)",
padding: "var(--dt-space-inset-md)",
background: t.bg,
border: `var(--dt-border-width-default) solid ${t.bd}`,
borderRadius: "var(--dt-radius-container)",
...style,
}}
{...rest}
>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke={t.fg} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ width: "var(--dt-size-icon-md)", height: "var(--dt-size-icon-md)", flex: "none", marginTop: 1 }}>
<path d={t.ring} />
{t.icon.split(" M").map((d, i) => <path key={i} d={i === 0 ? d : "M" + d} />)}
</svg>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--dt-space-stack-2xs)", flex: 1, minWidth: 0 }}>
{title && (
<span style={{ fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)", lineHeight: "var(--dt-text-label-md-line)", fontWeight: "var(--dt-font-weight-semibold)", color: "var(--dt-text-primary)" }}>{title}</span>
)}
{children && (
<span style={{ fontFamily: "var(--dt-text-body-sm-family)", fontSize: "var(--dt-text-body-sm-size)", lineHeight: "var(--dt-text-body-sm-line)", color: "var(--dt-text-secondary)" }}>{children}</span>
)}
{action && <div style={{ marginTop: "var(--dt-space-stack-2xs)" }}>{action}</div>}
</div>
{onDismiss && (
<button type="button" aria-label="Dismiss" onClick={onDismiss}
style={{ display: "flex", padding: 0, border: 0, background: "none", color: "var(--dt-text-secondary)", cursor: "pointer", flex: "none" }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
</button>
)}
</div>
);
}
In context
Alert also appears on the feedback reference cards, alongside the rest of its family.