# Handling states, validation, and accessibility ## Role You are implementing how the application responds to user actions and ensuring page-level accessibility. These patterns determine whether users trust the application. Follow them for every form, API call, and user action. Aura components handle many accessibility concerns automatically; you are responsible for composition, copy, focus, and page structure. Aura's focus system uses `shadow-focus-ring` (custom shadow token). Never remove or override it. All UI elements use Aura components and tokens. Error states use the destructive token family, warnings use warning tokens, success uses success tokens. For message wording patterns, see `writing-copy.md`. For all Storybook URLs, see `./storybook-links.md`. What Aura components handle automatically: | Concern | Aura handles | You verify | |---------|-------------|-----------| | Focus indicators | shadow-focus-ring on interactive elements | Not hidden by overflow or z-index | | Keyboard activation | Button: Enter/Space. Input: standard keys | Custom elements also respond | | ARIA roles | Correct roles on Dialog, Segmented Control (Tabs ARIA pattern), etc. | Custom components have roles | | Color contrast | Token pairs designed for AA compliance | Page backgrounds don't reduce contrast | | Dark mode | Semantic tokens adapt automatically | Custom colors also work in dark mode | | Disabled states | Communicated via aria-disabled | Reason for disabled is accessible | | Focus trapping | Dialog traps focus when open | You return focus to trigger on close | - Validate on blur, not on every keystroke - Show errors inline, adjacent to the field - Preserve user input on failure (never clear the form) - Move focus to first error field on submission failure - Announce errors to screen readers via aria-live | State | Background | Text | Border | |-------|-----------|------|--------| | Error | bg-destructive | text-destructive-foreground | border-destructive | | Warning | bg-warning | text-warning-foreground | — | | Success | bg-success | text-success-foreground | — | | Disabled | bg-disabled | text-disabled-foreground | — | | Field type | Correct message | Incorrect message | |-----------|----------------|-------------------| | Required | "Report name is required." | "Required" | | Email | "Email must include an @ symbol." | "Invalid" | | Password | "At least 8 characters." | "Too short" | | Number | "Value must be between 1 and 100." | "Invalid" | | Date | "End date must be after start date." | "Invalid date" | See `writing-copy.md` for full message patterns. Every form field must support the validation states applicable to its type. Use this table to determine which states to implement: | Field type | required | format | length | range | uniqueness | |-----------|----------|--------|--------|-------|------------| | Text Input | yes | — | optional | — | optional | | Email Input | yes | yes | — | — | optional | | Password | yes | yes | yes | — | — | | Number Input | yes | — | — | yes | — | | Date Picker | yes | — | — | yes | — | | Textarea | yes | — | yes | — | — | | Select | yes | — | — | — | — | | Combobox | yes | — | — | — | — | | Checkbox | — | — | — | — | — | | File Upload | yes | yes | — | yes (size) | — | "yes" = must implement. "optional" = implement if relevant. A complete form field with all states (default, focused, error, success, disabled): import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { HelperText } from '@/components/ui/helper-text'; {/* Default / Focused state */}
setName(e.target.value)} onBlur={validateName} aria-describedby="report-name-helper" aria-invalid={!!nameError} disabled={isSubmitting} /> {nameError ? ( {nameError} ) : ( A descriptive name for your report. )}
Key implementation details: - Label with required indicator (asterisk in text-destructive) - Input with aria-describedby linking to HelperText - Input with aria-invalid reflecting error state - Validation on blur via onBlur handler - HelperText swaps between hint (default) and error message - Disabled state during form submission
Any action taking more than 300ms must show a loading indicator using Aura components. | Context | Pattern | Aura component | |---------|---------|---------------| | Page load | Skeleton screen | Skeleton | | Button action | Button disabled + spinner | Button loading state | | Data refresh | Overlay spinner | Spinner on existing content | | Long operation | Progress bar + message | Progress | {/* Button loading during async action */} {/* Skeleton while page loads */} {isLoading ? (
) : ( )}
{/* No loading state */} {/* Blank screen while loading */} {isLoading ? null : }
Every API failure must show a user-facing message using Aura Alert component. Never fail silently. See `writing-copy.md` for message wording. import { Alert, AlertDescription } from '@/components/ui/alert'; import { AlertCircle } from 'lucide-react'; {error && ( {error} )} Use Sonner toast for brief confirmations. import { toast } from 'sonner'; // After save toast.success('Report saved successfully.'); // After delete toast.success('Report deleted.'); // After bulk action toast.success(`${count} items archived.`); // No feedback await saveReport(data); navigate('/reports'); // Vague toast.success('Done!'); Destructive actions must show Dialog with specific action verb. See `writing-copy.md` for copy. import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; Delete this report? This will permanently remove "{report.name}" and all associated data. This cannot be undone. {/* Yes/No, no description, wrong variant */} Confirm Are you sure?
- Tab order follows visual reading order - Every interactive element reachable via Tab - No keyboard traps - Skip-to-content link on pages with complex nav Skip to main content

Reports

{/* Content in logical tab order */}
Use heading levels (H1–H6) in strict sequential order. - One H1 per page (the page title) - Never skip levels (H1 directly to H3) - Never use heading tags for visual sizing — use text-* classes from the Typography foundation instead Aura applies text-undefined-foreground to headings by default. Every image needs alt. Icons in buttons need aria-label. | Type | Approach | Example | |------|----------|---------| | Informational | Describe content | alt="Chart: output up 20%" | | Decorative | Empty | alt="" | | Icon button | aria-label on parent | aria-label="Delete report" | | Icon with label | Hide icon | aria-hidden="true" on icon | {/* Icon + text: hide icon from screen reader */} {/* Icon only: label on button */} | Scenario | Method | |----------|--------| | Search results update | aria-live="polite" | | Form error | aria-live="assertive" | | Toast | Sonner handles this | | Dialog opens | Focus moves to dialog (Aura handles) | | Dialog closes | Return focus to trigger | {/* Screen reader announcement for filtered results */}
{results.length} results found for "{query}"
Never use color alone to convey meaning. {/* Status with text + color */} {/* Color only — invisible to colorblind users */}
Short sentences and simple grammar translate more reliably. Plan for text expansion in localized UIs (e.g. German often adds 30–40% length); allow flexible button and title widths. For automated checks, use WAVE, axe DevTools, or Lighthouse in Chrome DevTools. For manual verification, unplug the mouse and complete primary tasks with keyboard only; spot-check with VoiceOver (Mac) or NVDA (Windows) for critical flows. Before submitting any page: - [ ] Tab through all elements in logical order? - [ ] Every button/link works with Enter/Space? - [ ] Every dialog opens/closes with keyboard? - [ ] Escape closes dialogs, popovers, dropdowns? - [ ] Every image has appropriate alt text? - [ ] Every form field has a visible label? - [ ] Non-color indicator for every status? - [ ] Headings follow H1 → H2 → H3? - [ ] Dynamic updates announced to screen readers? - [ ] Focus ring (shadow-focus-ring) visible on all elements? **Forms and async** 1. Destructive action with undo? — Still confirm. Mention undo in body: "You can undo within 30 seconds." 2. Bulk delete? — One confirmation: "Delete 12 reports?" 3. Auto-save? — Subtle "Saved" indicator, not toast each time. 4. Error in multi-step flow? — Don't lose progress. Show error on current step. Let user retry. **Accessibility** 1. Complex data viz? — Text summary via alt or sr-only text. 2. Drag-and-drop? — Keyboard alternative required. 3. Real-time dashboard? — aria-live="polite", not "assertive". 4. Third-party embed? — iframe with descriptive title.