Skip to content

Alert

A block-level message attached to a region, with tone-appropriate live-region politeness.

FeedbackstarterFeaturedfeedbackmessagebannerlive-region

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

The exact file rendered in the preview above.

import { Button } from '@/components/ui/button'
import { Alert } from '@/components/ui/alert'
import { DemoColumn, DemoStage } from './_kit'

export default function AlertDemo() {
  return (
    <DemoStage>
      <DemoColumn width="lg">
        <Alert tone="info" title="Scheduled maintenance">
          The EU West region will be read-only on 14 March, 02:30–03:15 UTC.
        </Alert>

        <Alert tone="success" title="Domain verified">
          acme.com is now verified. Members with an @acme.com address can join automatically.
        </Alert>

        <Alert tone="warning" title="Approaching your seat limit">
          38 of 40 seats are in use. Adding members beyond 40 will change your invoice.
        </Alert>

        <Alert
          tone="danger"
          title="Deployment failed"
          actions={
            <>
              <Button size="sm" variant="destructive">
                Roll back
              </Button>
              <Button size="sm" variant="outline">
                View logs
              </Button>
            </>
          }
        >
          Build <code className="font-mono">a91f2c</code> exited with status 1 during the type-check
          step.
        </Alert>

        <Alert tone="neutral">
          Alerts can carry a body with no title when the message is a single sentence.
        </Alert>
      </DemoColumn>
    </DemoStage>
  )
}

components/demos/alert.tsx

import { Button } from '@/components/ui/button'
import { Alert } from '@/components/ui/alert'
import { DemoColumn, DemoStage } from './_kit'

export default function AlertDemo() {
  return (
    <DemoStage>
      <DemoColumn width="lg">
        <Alert tone="info" title="Scheduled maintenance">
          The EU West region will be read-only on 14 March, 02:30–03:15 UTC.
        </Alert>

        <Alert tone="success" title="Domain verified">
          acme.com is now verified. Members with an @acme.com address can join automatically.
        </Alert>

        <Alert tone="warning" title="Approaching your seat limit">
          38 of 40 seats are in use. Adding members beyond 40 will change your invoice.
        </Alert>

        <Alert
          tone="danger"
          title="Deployment failed"
          actions={
            <>
              <Button size="sm" variant="destructive">
                Roll back
              </Button>
              <Button size="sm" variant="outline">
                View logs
              </Button>
            </>
          }
        >
          Build <code className="font-mono">a91f2c</code> exited with status 1 during the type-check
          step.
        </Alert>

        <Alert tone="neutral">
          Alerts can carry a body with no title when the message is a single sentence.
        </Alert>
      </DemoColumn>
    </DemoStage>
  )
}

components/ui/alert.tsx

import type { ReactNode } from 'react'
import { Info, CheckCircle2, AlertTriangle, OctagonAlert } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Alert
 *
 * A block-level message attached to a region of the page. The icon is chosen
 * from the tone, and `role` switches to `alert` for danger so screen readers
 * interrupt — informational tones use the polite `status` role instead.
 */
export type AlertTone = 'info' | 'success' | 'warning' | 'danger' | 'neutral'

export interface AlertProps {
  tone?: AlertTone
  title?: ReactNode
  children?: ReactNode
  /** Rendered under the body — usually one or two buttons. */
  actions?: ReactNode
  /** Slot for a dismiss control supplied by the caller. */
  trailing?: ReactNode
  className?: string
  /** Force the live-region politeness rather than deriving it from tone. */
  live?: 'off' | 'polite' | 'assertive'
}

const tones = {
  info: { icon: Info, surface: 'bg-info-soft border-info-line', accent: 'text-info' },
  success: {
    icon: CheckCircle2,
    surface: 'bg-success-soft border-success-line',
    accent: 'text-success',
  },
  warning: {
    icon: AlertTriangle,
    surface: 'bg-warning-soft border-warning-line',
    accent: 'text-warning',
  },
  danger: {
    icon: OctagonAlert,
    surface: 'bg-danger-soft border-danger-line',
    accent: 'text-danger',
  },
  neutral: { icon: Info, surface: 'bg-surface-sunken border-line', accent: 'text-ink-muted' },
} as const

export function Alert({
  tone = 'info',
  title,
  children,
  actions,
  trailing,
  className,
  live,
}: AlertProps) {
  const entry = tones[tone]
  const Icon = entry.icon
  const politeness = live ?? (tone === 'danger' ? 'assertive' : 'polite')

  return (
    <div
      role={tone === 'danger' ? 'alert' : 'status'}
      aria-live={politeness === 'off' ? undefined : politeness}
      className={cn('flex gap-3 rounded-md border p-3 text-sm', entry.surface, className)}
    >
      <Icon className={cn('mt-0.5 size-4 shrink-0', entry.accent)} aria-hidden="true" />
      <div className="min-w-0 flex-1">
        {title ? <p className="font-semibold text-ink-strong">{title}</p> : null}
        {children ? <div className={cn('text-ink-muted', title && 'mt-1')}>{children}</div> : null}
        {actions ? <div className="mt-3 flex flex-wrap gap-2">{actions}</div> : null}
      </div>
      {trailing ? <div className="shrink-0">{trailing}</div> : null}
    </div>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

For messages that belong to a place on the page and should stay there. If the message is transient and unrelated to a region, use Toast instead.

  • Danger uses `role="alert"` and interrupts; every other tone uses the polite `role="status"`.
  • Put the recovery action in the alert — an error without a next step is just bad news.
  • Never rely on the tint alone: each tone ships a distinct icon and a title.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Info
  • Success
  • Warning
  • Danger
  • Neutral
  • With actions

Accessibility

Politeness
Derived from tone by default, and overridable with `live` when the caller knows better.
Icons
Icons are `aria-hidden`; the tone is carried by the words.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.