Alert
Keep contextual information and a useful next step near the task; choose announcement urgency deliberately.
Choose the right message
Use an inline alert when the information belongs to the current task and must remain available. A toast can supplement it but should not become the only place to read an error or recover a failed action. AlertDialog is a separate, interrupting confirmation mechanism.
Alert defaults to role=alert for urgent announcement. For ordinary dynamically updated feedback, supply role=status; for static contextual information, use a suitable non-live role such as note. Visual variant and announcement priority are independent decisions.
| Situation | Treatment | Recovery |
|---|---|---|
| Helpful context | default or info; non-live static content | Link the source or explain the constraint. |
| Local outcome | success only after an actual result | Retain the result in the task, not just a green box. |
| Potential consequence | warning plus explicit wording | Give a clear next step; do not block unrelated work unnecessarily. |
| Failure | error/destructive plus a specific message | Retain input and provide retry or an alternative. |
Anatomy and specifications
| Part | Current implementation | Author responsibility |
|---|---|---|
| Root | 1rem padding, 2px border, --radius-lg | Readable source-context and appropriate role. |
| Title / description | 16px title and --text-sm description | Titles are paragraph leads by default. Supply headingLevel only when this message creates a real document section. |
| Tone | Six variants; semantic tinted backgrounds and borders | Meaning remains in text. Titles inherit readable foreground instead of low-contrast tone colors. |
| Actions | Wrapping flex region, optional caller action | Real action outcome and enough space on mobile. |
| Dismiss | 44×44px named type=button | Hiding the message does not resolve the underlying condition; restore focus if removed. |
Working message and recovery
These specimens describe local examples. Their color does not establish review, consent or publication.
default example
Keep the source and the next step understandable.
info example
Keep the source and the next step understandable.
success example
Keep the source and the next step understandable.
warning example
Keep the source and the next step understandable.
error example
Keep the source and the next step understandable.
destructive example
Keep the source and the next step understandable.
'use client';
import { useRef, useState } from 'react';
import { Alert, Badge, Button, VisuallyHidden, type AlertProps, type BadgeProps } from '@mobtranslate/ui';
const alertVariants: NonNullable<AlertProps['variant']>[] = ['default', 'info', 'success', 'warning', 'error', 'destructive'];
const badgeVariants: NonNullable<BadgeProps['variant']>[] = ['default', 'primary', 'secondary', 'destructive', 'error', 'outline', 'success', 'warning'];
export function AlertCommunicationExample() {
const [visible, setVisible] = useState(false);
const restore = useRef<HTMLElement>(null);
const [result, setResult] = useState('');
const [headingLevel, setHeadingLevel] = useState<AlertProps['headingLevel']>();
return <div data-alert-lab className="space-y-4">
<p>These specimens describe local examples. Their color does not establish review, consent or publication.</p>
<label className="block" htmlFor="alert-title-structure">Alert title structure</label>
<select id="alert-title-structure" className="mt-input" value={headingLevel ?? 'paragraph'} onChange={event => setHeadingLevel(event.target.value === 'paragraph' ? undefined : 3)}><option value="paragraph">Paragraph lead</option><option value="3">Level 3 heading under this section</option></select>
<div className="space-y-3">{alertVariants.map(variant => <Alert key={variant} variant={variant} role="note" title={`${variant} example`} headingLevel={headingLevel} description="Keep the source and the next step understandable." data-tone={variant} />)}</div>
<Button ref={restore} type="button" onClick={() => { setVisible(true); setResult(''); }}>Show recoverable alert</Button>
{visible && <Alert role="status" variant="warning" title="Local task paused" description="Your example input is retained." dismissible dismissLabel="Dismiss local task message"
onDismiss={() => { setVisible(false); setResult('Message dismissed; the local task remains paused.'); restore.current?.focus(); }}
action={<Button type="button" variant="secondary" onClick={() => { setVisible(false); setResult('Local task resumed. No request was sent.'); restore.current?.focus(); }}>Resume local task</Button>}>
<p className="text-sm">Additional child content stays visible beside shorthand title and description.</p>
</Alert>}
<p role="status">{result}</p>
</div>;
}
export function BadgeCommunicationExample() {
const [count, setCount] = useState(1);
const [messages, setMessages] = useState<{id:number;subject:string}[]>([]);
const [unread, setUnread] = useState(0);
const [inboxOpen, setInboxOpen] = useState(false);
const inboxTrigger=useRef<HTMLElement>(null);
const messageId=useRef(0);
const [shape, setShape] = useState<NonNullable<BadgeProps['shape']>>('pill');
return <div data-badge-lab className="space-y-4">
<label className="block">Badge shape <select value={shape} onChange={event => setShape(event.target.value as NonNullable<BadgeProps['shape']>)} className="mt-input"><option value="pill">Pill</option><option value="rounded">Rounded</option></select></label>
<div className="flex flex-wrap gap-3">{badgeVariants.map(variant => <Badge key={variant} variant={variant} shape={shape} data-tone={variant}>{variant} example</Badge>)}</div>
<label className="block">Local task count <input type="range" min="0" max="120" value={count} onChange={event => setCount(Number(event.target.value))} className="block w-full" /></label>
<p role="status" aria-atomic="true">{count === 0 ? 'No local tasks.' : <>Local tasks: <Badge data-count-badge shape={shape}><span aria-hidden="true">{count > 99 ? '99+' : count}</span><VisuallyHidden>{count} {count === 1 ? 'task' : 'tasks'}</VisuallyHidden></Badge></>}</p>
<p className="text-sm">The compact count caps at 99+ visually; its text alternative retains the actual count. This badge is read-only, not a filter or a review credential.</p>
<h3>Local inbox and unread ownership</h3>
<div className="flex flex-wrap gap-3">
<Button type="button" variant="outline" onClick={()=>{const id=++messageId.current;setMessages(items=>[...items,{id,subject:`Local example notification ${id}`}]);setUnread(current=>inboxOpen?0:current+1);}}>Add local inbox notification</Button>
<Button ref={inboxTrigger} data-inbox-trigger type="button" variant="outline" aria-label={`Open local inbox, ${unread?`${unread} unread ${unread===1?'notification':'notifications'}`:'no unread notifications'}`} aria-expanded={inboxOpen} aria-controls="badge-local-inbox" onClick={()=>{setInboxOpen(true);setUnread(0);}}>
<span aria-hidden="true" data-inbox-icon style={{position:'relative',display:'inline-block',width:24,height:24,flexShrink:0}}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{position:'absolute',insetInlineStart:0,insetBlockEnd:0}}><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 6 9 7 9-7"/></svg>
{unread>0&&<span data-inbox-dot style={{position:'absolute',insetBlockStart:0,insetInlineEnd:0,width:6,height:6,borderRadius:'50%',background:'var(--color-error)'}}/>}
</span>Open local inbox
</Button>
</div>
<section id="badge-local-inbox" aria-label="Local inbox" hidden={!inboxOpen}>
<p>{messages.length?`${messages.length} retained local notifications. Opening marks them read; it does not delete them.`:'No local notifications yet.'}</p>
<ul>{messages.map(message=><li key={message.id}>{message.subject}</li>)}</ul>
<Button type="button" variant="outline" onClick={()=>{setInboxOpen(false);inboxTrigger.current?.focus();}}>Close local inbox</Button>
</section>
</div>;
}
API reference
| Export / prop | Contract |
|---|---|
| Alert | HTML div props and ref; default role=alert, override via role. |
| variant | default | info | success | warning | error | destructive; default |
| title / description | Optional strings; shortcut title is a paragraph lead by default, description is p. |
| icon | ReactNode; hide decorative icons from accessibility APIs. |
| children | Additional content; retained with shorthand title/description. |
| action | ReactNode rendered in the action region even without shorthand text. |
| dismissible / onDismiss | Button appears when both are supplied; caller controls visibility. |
| dismissLabel | string; Dismiss alert default; name the specific context when needed. |
| headingLevel | Optional 1–6 on Alert or AlertTitle; choose the level from the surrounding document outline. Absent renders p. |
| AlertTitle | p by default; h1–h6 when headingLevel is supplied; native element props and HTMLElement ref. |
| AlertDescription | p with mt-alert-description; native paragraph props/ref. |
| className / style / native props | Passed and merged; no automatic request, retry or dismissal state. |
Behavior and accessibility
- Do not announce every existing page message urgently on arrival. Use live regions for meaningful changes and avoid duplicate focused-summary/alert announcements.
- The component does not own open state. onDismiss must update the application and choose an enabled focus destination.
- Keep the reason, effect and next action readable without color or a tooltip.
- Distinguish dismissing a notice from cancelling a request, clearing an error or changing permission.
- An action must remain understandable alongside a long description and at 320 CSS px.
Failure cases
- An unnamed icon dismiss button, or a dismiss control that accidentally submits a form.
- Children disappearing because a title shorthand was supplied.
- Warning-colored small text with insufficient contrast on its tinted surface.
- Removing the only recovery action or asserting that an error is fixed merely because its message is hidden.
Verification and release scope
- Operate the checked example with keyboard and pointer; inspect the actual name, role, relationships and focus destination.
- Check both themes, narrow reflow, long labels, reduced motion and forced colors. Text contrast and meaningful graphic contrast are separate checks.
- Test the relevant waiting, completion, failure, cancellation and recovery paths. A local simulation is not a verified backend result.
- Use a named screen-reader/browser pair to verify spoken output. DOM attributes and automated passes do not prove the announcement.
- Retain source, review and permission evidence when applying these controls to a real language workflow. Color and motion cannot establish authority.