MobTranslate styleguide

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.

SituationTreatmentRecovery
Helpful contextdefault or info; non-live static contentLink the source or explain the constraint.
Local outcomesuccess only after an actual resultRetain the result in the task, not just a green box.
Potential consequencewarning plus explicit wordingGive a clear next step; do not block unrelated work unnecessarily.
Failureerror/destructive plus a specific messageRetain input and provide retry or an alternative.

Anatomy and specifications

PartCurrent implementationAuthor responsibility
Root1rem padding, 2px border, --radius-lgReadable source-context and appropriate role.
Title / description16px title and --text-sm descriptionTitles are paragraph leads by default. Supply headingLevel only when this message creates a real document section.
ToneSix variants; semantic tinted backgrounds and bordersMeaning remains in text. Titles inherit readable foreground instead of low-contrast tone colors.
ActionsWrapping flex region, optional caller actionReal action outcome and enough space on mobile.
Dismiss44×44px named type=buttonHiding 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.

tsx
'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 / propContract
AlertHTML div props and ref; default role=alert, override via role.
variantdefault | info | success | warning | error | destructive; default
title / descriptionOptional strings; shortcut title is a paragraph lead by default, description is p.
iconReactNode; hide decorative icons from accessibility APIs.
childrenAdditional content; retained with shorthand title/description.
actionReactNode rendered in the action region even without shorthand text.
dismissible / onDismissButton appears when both are supplied; caller controls visibility.
dismissLabelstring; Dismiss alert default; name the specific context when needed.
headingLevelOptional 1–6 on Alert or AlertTitle; choose the level from the surrounding document outline. Absent renders p.
AlertTitlep by default; h1–h6 when headingLevel is supplied; native element props and HTMLElement ref.
AlertDescriptionp with mt-alert-description; native paragraph props/ref.
className / style / native propsPassed 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.