Alert

An inline message about the state of the page or a form. It sits in the layout and stays until the condition clears.

Feedback Alert.jsx Alert.d.ts Alert.md

Playground

Change a prop and watch it. The code below follows along.

Install 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 error prop.
  • 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.
TokenTierDeclared as
--dt-border-dangersemanticvar(--dt-color-red-200)
--dt-border-infosemanticvar(--dt-color-cyan-200)
--dt-border-successsemanticvar(--dt-color-green-200)
--dt-border-warningsemanticvar(--dt-color-amber-200)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-radius-containersemanticvar(--dt-radius-raw-16)
--dt-size-icon-mdsemanticvar(--dt-dim-5)
--dt-space-inline-smsemanticvar(--dt-dim-3)
--dt-space-inset-mdsemanticvar(--dt-dim-4)
--dt-space-stack-2xssemanticvar(--dt-dim-1)
--dt-surface-danger-subtlesemanticvar(--dt-color-red-050)
--dt-surface-info-subtlesemanticvar(--dt-color-cyan-050)
--dt-surface-success-subtlesemanticvar(--dt-color-green-050)
--dt-surface-warning-subtlesemanticvar(--dt-color-amber-050)
--dt-text-body-sm-familysemanticvar(--dt-font-family-sans)
--dt-text-body-sm-linesemanticvar(--dt-line-height-sm)
--dt-text-body-sm-sizesemanticvar(--dt-font-size-sm)
--dt-text-dangersemanticvar(--dt-color-red-800)
--dt-text-infosemanticvar(--dt-color-cyan-900)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-linesemanticvar(--dt-line-height-sm)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-successsemanticvar(--dt-color-green-800)
--dt-text-warningsemanticvar(--dt-color-amber-900)
--dt-font-weight-semiboldprimitive600

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.