ToastRegion
Confirms that something happened, out of the user's way. Use it for results of actions
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall and import
ToastRegion is in @dovetail-ds/react. Install it with React, if you haven't:
npm install @dovetail-ds/react react react-dom
import { ToastRegion } 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
Confirms that something happened, out of the user's way. Use it for results of actions the user just took.
Rules
- Render toasts inside a
ToastRegion. It owns the fixed positioning, stacking, and the accessible region name. - Toasts are for outcomes, not errors that need a decision. A failed save that the user must resolve belongs in an Alert next to the form.
- One action, and it should be Undo or Retry. A toast that disappears while the user reaches for a link is a broken control.
dangerannounces assertively and should not auto-dismiss. Everything else can.- Never stack more than three. Collapse the rest into a count.
Surfaces
surface="glass" or "glass-strong" for a toast over a picture or a busy screen. The tone still sets its edge and icon.
Tradeoffs
Toasts are easy to miss: they appear away from the point of action and vanish. For anything the user must acknowledge, use a Dialog or an inline Alert.
Props
The typed contract, from Toast.d.ts.
import * as React from "react";
/** Transient confirmation or failure notice. */
export interface ToastProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
/** @default "neutral" */
tone?: "neutral" | "success" | "warning" | "danger";
/** "glass" lets what's behind show through, blurred (--dt-surface-glass with --dt-backdrop-glass); "glass-strong" lets less through. Use over a picture or a busy screen. @default "raised" */
surface?: "raised" | "glass" | "glass-strong";
title?: React.ReactNode;
children?: React.ReactNode;
icon?: React.ReactNode;
/** One action, usually Undo. */
action?: React.ReactNode;
onDismiss?: () => void;
/** @default "Dismiss" */
dismissLabel?: string;
}
/** Fixed container that stacks toasts in a corner. */
export interface ToastRegionProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
/** @default "bottom-right" */
placement?: "bottom-right" | "bottom-left" | "top-right" | "top-center";
/** @default "Notifications" */
label?: string;
}
export declare function Toast(props: ToastProps): React.JSX.Element;
export declare function ToastRegion(props: ToastRegionProps): React.JSX.Element;
Tokens it reads
Every custom property Toast.jsx resolves, read from the source: 30 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-backdrop-glass | semantic | saturate(1.6) blur(var(--dt-blur-glass)) |
--dt-border-danger | semantic | var(--dt-color-red-200) |
--dt-border-default | semantic | var(--dt-color-neutral-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-elevation-3 | semantic | var(--dt-shadow-raw-3) |
--dt-radius-control | semantic | var(--dt-radius-raw-8) |
--dt-radius-overlay | semantic | var(--dt-radius-raw-24) |
--dt-space-inline-sm | semantic | var(--dt-dim-3) |
--dt-space-inset-2xs | semantic | var(--dt-dim-1) |
--dt-space-inset-lg | semantic | var(--dt-dim-6) |
--dt-space-inset-md | semantic | var(--dt-dim-4) |
--dt-space-inset-sm | semantic | var(--dt-dim-3) |
--dt-space-stack-xs | semantic | var(--dt-dim-2) |
--dt-surface-glass | semantic | color-mix(in oklab, var(--dt-surface-overlay) 72%, transparent) |
--dt-surface-glass-strong | semantic | color-mix(in oklab, var(--dt-surface-overlay) 88%, transparent) |
--dt-surface-raised | semantic | var(--dt-color-white) |
--dt-text-body-md-size | semantic | var(--dt-font-size-md) |
--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-label-md-family | semantic | var(--dt-font-family-secondary) |
--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-tertiary | semantic | var(--dt-color-neutral-500) |
--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 = {
neutral: { bd: "var(--dt-border-default)", fg: "var(--dt-text-secondary)" },
success: { bd: "var(--dt-border-success)", fg: "var(--dt-text-success)" },
warning: { bd: "var(--dt-border-warning)", fg: "var(--dt-text-warning)" },
danger: { bd: "var(--dt-border-danger)", fg: "var(--dt-text-danger)" },
};
/* Glass: the overlay surface let through, blurred, for a panel over a
picture or a busy screen. Always paired with the backdrop filter. */
const GLASS = { glass: "var(--dt-surface-glass)", "glass-strong": "var(--dt-surface-glass-strong)" };
export function Toast({ tone = "neutral", title, children, icon, action, onDismiss, dismissLabel = "Dismiss", surface = "raised", style, ...rest }) {
const t = TONES[tone] || TONES.neutral;
return (
<div role={tone === "danger" ? "alert" : "status"} aria-live={tone === "danger" ? "assertive" : "polite"} style={{
display: "flex", alignItems: "flex-start", gap: "var(--dt-space-inline-sm)",
padding: "var(--dt-space-inset-sm) var(--dt-space-inset-md)",
background: GLASS[surface] || "var(--dt-surface-raised)", color: "var(--dt-text-primary)",
backdropFilter: GLASS[surface] ? "var(--dt-backdrop-glass)" : undefined,
WebkitBackdropFilter: GLASS[surface] ? "var(--dt-backdrop-glass)" : undefined,
border: `var(--dt-border-width-default) solid ${t.bd}`,
borderRadius: "var(--dt-radius-overlay)", boxShadow: "var(--dt-elevation-3)",
minWidth: 260, maxWidth: 420, boxSizing: "border-box", ...style,
}} {...rest}>
{icon && <span aria-hidden="true" style={{ color: t.fg, flex: "none", display: "flex", marginTop: 1 }}>{icon}</span>}
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 2 }}>
{title && <span style={{ fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)", fontWeight: "var(--dt-font-weight-semibold)" }}>{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)", textWrap: "pretty" }}>{children}</span>}
</div>
{action && <span style={{ flex: "none" }}>{action}</span>}
{onDismiss && (
<button type="button" onClick={onDismiss} aria-label={dismissLabel} style={{
flex: "none", appearance: "none", background: "transparent", border: "none", cursor: "pointer",
color: "var(--dt-text-tertiary)", fontSize: "var(--dt-text-body-md-size)", lineHeight: 1,
padding: "var(--dt-space-inset-2xs, 4px)", borderRadius: "var(--dt-radius-control)",
}}>×</button>
)}
</div>
);
}
export function ToastRegion({ children, placement = "bottom-right", label = "Notifications", style, ...rest }) {
const pos = {
"bottom-right": { bottom: "var(--dt-space-inset-lg)", right: "var(--dt-space-inset-lg)", alignItems: "flex-end" },
"bottom-left": { bottom: "var(--dt-space-inset-lg)", left: "var(--dt-space-inset-lg)", alignItems: "flex-start" },
"top-right": { top: "var(--dt-space-inset-lg)", right: "var(--dt-space-inset-lg)", alignItems: "flex-end" },
"top-center": { top: "var(--dt-space-inset-lg)", left: "50%", transform: "translateX(-50%)", alignItems: "center" },
}[placement];
return (
<div role="region" aria-label={label} style={{
position: "fixed", zIndex: 50, display: "flex", flexDirection: "column",
gap: "var(--dt-space-stack-xs)", pointerEvents: "none", ...pos, ...style,
}} {...rest}>
{React.Children.map(children, c => <div style={{ pointerEvents: "auto" }}>{c}</div>)}
</div>
);
}
In context
ToastRegion also appears on the feedback reference cards, alongside the rest of its family.