Skip to main content

UX Standards — 29: Toast and Alert Standards

Governs: All user feedback messages — transient toasts, persistent alerts, inline banners, and error display. Parent rules: See 00-OVERVIEW-AND-CSS-RULES.md first.


The Four Feedback Mechanisms

MechanismPersistenceBlockingWhen To Use
toast()Transient (auto-dismiss)NoAction confirmation, background job status
Alert (shadcn)Persistent until dismissed or fixedNoPage-level warnings affecting workflow
InfoBannerPersistent, dismissibleNoAnnouncements, feature notices
Inline field error (FormMessage)Until field is correctedNoForm validation errors

Golden Rule: Field validation errors NEVER go in a toast. Toast is never for form errors.


1. Toast (useToast)

const { toast } = useToast();

// Success
toast({
title: t('toast.saved'),
description: t('toast.bookingCreated'),
duration: 4000,
});

// Error (recoverable)
toast({
title: t('toast.error'),
description: errorMessage,
variant: 'destructive',
duration: 8000,
});

// With action (e.g., undo)
toast({
title: t('toast.deleted'),
description: t('toast.bookingDeleted'),
action: (
<ToastAction altText={t('common.undo')} onClick={handleUndo}>
{t('common.undo')}
</ToastAction>
),
duration: 6000,
});

// Long-running job started
toast({
title: t('toast.exportStarted'),
description: t('toast.exportDescription'),
duration: 4000,
});

Duration Standards

ScenarioDuration
Success confirmation4000 ms
Info (job started, etc.)4000 ms
Warning6000 ms
Error (recoverable)8000 ms
Critical errorInfinity (manual dismiss) — use Alert instead

Toast Position

Bottom-right. Configured once in root layout via <Toaster />. Never change position per-toast.

When To Use Toast

✅ Record saved / created / deleted successfully ✅ Export/import started in background ✅ Copy to clipboard successful ✅ Link/invite sent ✅ Session about to expire warning ❌ Form validation errors (use FormMessage) ❌ Critical system errors (use Alert) ❌ Information the user needs to read carefully (use Alert or InfoBanner)


2. Alert Component (Persistent Page-Level)

import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';

// Warning — configuration issue
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t('alert.configurationIncomplete')}</AlertTitle>
<AlertDescription>
{t('alert.configurationDescription')}
<Button variant="link" className="p-0 h-auto" asChild>
<Link href="/settings">{t('alert.goToSettings')}</Link>
</Button>
</AlertDescription>
</Alert>

// Destructive — blocking error
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>{t('alert.syncFailed')}</AlertTitle>
<AlertDescription>{errorDetail}</AlertDescription>
</Alert>

When To Use Alert

✅ API connection failing — user needs to fix before proceeding ✅ Required configuration missing ✅ Form submission failed (server error, non-field) ✅ Batch import has warnings the user should review ✅ Record is in a locked/cancelled state that affects editing ❌ Transient confirmations (use toast) ❌ General announcements (use InfoBanner)

Placement: Immediately above the affected content (above the form, above the table). Never float.


3. InfoBanner (Shared Component)

// packages/shared/components/shared/info-banner.tsx
interface InfoBannerProps {
id: string; // unique ID for localStorage dismiss persistence
title: string;
description?: string;
variant?: 'info' | 'warning' | 'success';
action?: { label: string; href: string };
}

// Usage
<InfoBanner
id="new-export-feature-2026-03"
title={t('banner.newExportTitle')}
description={t('banner.newExportDescription')}
variant="info"
action={{ label: t('banner.learnMore'), href: '/docs/export' }}
/>

InfoBanner renders at the TOP of page content (below page header, above stat cards). It includes an X dismiss button. Dismissal stored in localStorage so it doesn't reappear after the user closes it.

Variants

VariantIconLeft border
infoInfoborder-l-4 border-info
warningAlertTriangleborder-l-4 border-warning
successCheckCircle2border-l-4 border-success

4. Inline Field Error

Always via FormMessage in React Hook Form. Never customized.

<FormField render={({ field }) => (
<FormItem>
<FormLabel>{t('field.name')}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage /> {/* Renders automatically from Zod resolver */}
</FormItem>
)} />

Style: text-xs text-destructive (built into FormMessage). Appears below input. No icon needed — color communicates error state.


Error Hierarchy (Decision Tree)

An error occurred — which mechanism?

Is it a FORM FIELD error (validation)?
→ FormMessage (inline below field)

Is it a SERVER/SUBMISSION error (non-field)?
→ Alert variant="destructive" above form actions

Is it a BACKGROUND JOB failure (async)?
→ toast variant='destructive' duration=8000
→ AND NotificationToastMonitor fires critical notification toast

Is it a PAGE-LEVEL configuration problem?
→ Alert at top of content area

Is it a FEATURE ANNOUNCEMENT?
→ InfoBanner (dismissible)

Is it ACTION CONFIRMATION (save, delete, copy)?
→ toast (success or destructive, 4-8s)

Violation Checklist

  • Form validation errors use FormMessage — NEVER toast
  • Toast duration: success=4000, warning=6000, error=8000
  • Critical errors that require user action use Alert, not toast
  • Alert variant="destructive" always has AlertCircle icon
  • InfoBanner has a persistent dismiss (localStorage keyed by id)
  • Toast position is bottom-right (never overridden)
  • <Toaster /> mounted once in root layout
  • Server submission errors use Alert above form actions
  • Toast action button present when "Undo" is possible
  • All toast titles/descriptions go through t() — no hardcoded English