v1.0

Alert

Preview

Tones

Loading…

Preview

Dismissible

Loading…

Preview

Code

ts
import Alert from '@/components/layout/Alert';

src/components/layout/Alert.tsx

AI prompt

text
Build an inline alert (callout) component in React + TypeScript + Tailwind CSS.

A message that stays on the page, for a condition the user has to read before acting. Transient feedback belongs in a toast, not here.

## Look
- Container: `flex gap-2.5 rounded-lg border px-3 py-2.5`, tinted per tone:
  - `info` (lucide `Info`): `border-indigo-200 bg-indigo-50 text-indigo-900`, dark `border-indigo-500/30 bg-indigo-500/10 text-indigo-200`
  - `success` (`CircleCheck`): the same recipe in emerald
  - `warning` (`TriangleAlert`): amber
  - `danger` (`CircleAlert`): rose
- Icon: 16px, `mt-0.5 shrink-0`, inherits the tone's text colour.
- Text block `min-w-0 flex-1 text-xs leading-relaxed`: an optional `font-semibold` title, then the body at `opacity-90` (`mt-0.5` when there is a title).
- Dismiss: when `onDismiss` is passed, a 14px `X` button at the right, `shrink-0 opacity-60 hover:opacity-100`, aria-label "Dismiss". Without it the alert is permanent.

## API
`tone?: 'info' | 'success' | 'warning' | 'danger'` ('info'), `title?: ReactNode`, `children?`, `onDismiss?: () => void`, `className`.

## Accessibility
`role="alert"` for `danger` only; the other tones use `role="status"`. The icon is `aria-hidden`.

## Demo
A stack: info "Scheduled maintenance — Read replicas are read-only until 02:00 UTC."; success "Invite sent — Alan Turing will receive an email shortly."; warning "Approaching your seat limit — 7 of 8 seats in use."; a dismissible danger "Payment failed — Update the card on file to avoid suspension."

## House style (applies to everything above)
- Stack: React 19 + TypeScript + Tailwind CSS v4, icons from lucide-react. One self-contained file; default-export the component and named-export its types. `'use client'` if it has state, refs or handlers.
- Font Inter; palette indigo on slate. Primary accent indigo-600 (hover indigo-700, dark mode indigo-400). Body text slate-700 / dark slate-200; secondary slate-500 / dark slate-400.
- Dark mode is a `.dark` class on <html> (not prefers-color-scheme). Every colour needs its `dark:` pair.
- Compact admin scale: text-xs (12px) for controls and body, 10–11px for meta, rounded-lg (8px) controls, rounded-2xl (16px) cards.
- Card surface ("panel"): `bg-white/60 dark:bg-slate-800/60 backdrop-blur-xl border border-white/60 dark:border-slate-700/60 rounded-2xl shadow-lg`, on a soft slate gradient page background.
- Floating surfaces (dropdowns, popovers, menus) are OPAQUE: `bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-2xl shadow-lg`, no backdrop blur (it creates a stacking context that traps the popover's z-index). In-flow popovers are z-50; portalled overlays z-200.
- Text inputs and select triggers: `w-full px-3 py-2 text-xs rounded-lg border border-slate-300 dark:border-slate-700 bg-white/80 dark:bg-slate-900/60 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500/40 focus:border-indigo-500`.
- Field labels: 11px semibold slate-600. Section titles: 10px semibold uppercase wide-tracking slate-500.
- Primary button: indigo-600 fill, white 12px semibold text, rounded-lg, px-3 py-2, disabled at 50% opacity. Ghost button: slate-600 text, hover slate-100.
- Popovers close on outside click AND on Escape (listen to both; include the portalled panel's element in the outside-click check).
- Don't nest scroll containers around popovers: an ancestor with overflow hidden/auto clips an absolutely-positioned dropdown. Portal the panel to <body> when it must escape a scroller, and reposition it on scroll and resize.
- Accessible by default: visible focus rings, keyboard support that matches the WAI-ARIA pattern for the widget, `aria-label` on icon-only buttons, `min-w-0` so text truncates instead of overflowing.

Source

tsx
'use client';

import { CircleAlert, CircleCheck, Info, TriangleAlert, X } from 'lucide-react';
import { cn } from '@/lib/cn';

const TONE = {
  info: { cls: 'border-indigo-200 bg-indigo-50 text-indigo-900 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-200', Icon: Info },
  success: { cls: 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-500/30 dark:bg-emerald-500/10 dark:text-emerald-200', Icon: CircleCheck },
  warning: { cls: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-200', Icon: TriangleAlert },
  danger: { cls: 'border-rose-200 bg-rose-50 text-rose-900 dark:border-rose-500/30 dark:bg-rose-500/10 dark:text-rose-200', Icon: CircleAlert },
} as const;

/**
 * An inline message that stays on the page — for a condition the user has to
 * read before acting. Transient feedback belongs in a toast, not here.
 */
export default function Alert({
  tone = 'info',
  title,
  onDismiss,
  className,
  children,
}: {
  tone?: keyof typeof TONE;
  title?: React.ReactNode;
  onDismiss?: () => void;
  className?: string;
  children?: React.ReactNode;
}) {
  const { cls, Icon } = TONE[tone];
  return (
    <div role={tone === 'danger' ? 'alert' : 'status'} className={cn('flex gap-2.5 rounded-lg border px-3 py-2.5', cls, className)}>
      <Icon className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
      <div className="min-w-0 flex-1 text-xs leading-relaxed">
        {title && <p className="font-semibold">{title}</p>}
        {children && <div className={cn(title ? "mt-0.5" : null, "opacity-90")}>{children}</div>}
      </div>
      {onDismiss && (
        <button onClick={onDismiss} aria-label="Dismiss" className="shrink-0 opacity-60 hover:opacity-100">
          <X className="h-3.5 w-3.5" />
        </button>
      )}
    </div>
  );
}

Props

PropTypeDefaultDescription
tonekeyof typeof TONE'info'
titleReact.ReactNode—
onDismiss() => void—
classNamestring—
childrenReact.ReactNode—