Files
md-files/.claude/skills/design/handling-states.md
T
2026-05-31 20:25:41 +00:00

427 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
<aura-coverage priority="high">
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 |
</aura-coverage>
<patterns>
<pattern name="form-validation" priority="critical">
<instruction>
- 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
</instruction>
<status-token-mapping>
| 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 | — |
</status-token-mapping>
<message-patterns>
| 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.
</message-patterns>
<field-validation-states>
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.
</field-validation-states>
<complete-field-example>
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 */}
<div className="space-y-2">
<Label htmlFor="report-name">
Report name <span className="text-destructive">*</span>
</Label>
<Input
id="report-name"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={validateName}
aria-describedby="report-name-helper"
aria-invalid={!!nameError}
disabled={isSubmitting}
/>
{nameError ? (
<HelperText id="report-name-helper" variant="error">
{nameError}
</HelperText>
) : (
<HelperText id="report-name-helper">
A descriptive name for your report.
</HelperText>
)}
</div>
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
</complete-field-example>
</pattern>
<pattern name="loading-states" priority="critical">
<instruction>
Any action taking more than 300ms must show a loading indicator using Aura components.
</instruction>
<variants>
| 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 |
</variants>
<example type="correct">
{/* Button loading during async action */}
<Button
variant="default"
onClick={handleSave}
disabled={isSaving}
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
'Save changes'
)}
</Button>
{/* Skeleton while page loads */}
{isLoading ? (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
) : (
<DataTable data={reports} columns={columns} />
)}
</example>
<example type="incorrect">
{/* No loading state */}
<Button onClick={handleSave}>Save changes</Button>
{/* Blank screen while loading */}
{isLoading ? null : <DataTable data={reports} />}
</example>
</pattern>
<pattern name="error-states" priority="critical">
<instruction>
Every API failure must show a user-facing message using Aura Alert component. Never fail silently.
See `writing-copy.md` for message wording.
</instruction>
<full-example>
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{error}
</AlertDescription>
<Button
variant="secondary"
size="sm"
onClick={retry}
className="ml-auto"
>
Try again
</Button>
</Alert>
)}
</full-example>
</pattern>
<pattern name="success-feedback" priority="high">
<instruction>
Use Sonner toast for brief confirmations.
</instruction>
<example type="correct">
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.`);
</example>
<example type="incorrect">
// No feedback
await saveReport(data);
navigate('/reports');
// Vague
toast.success('Done!');
</example>
</pattern>
<pattern name="confirmation-dialogs" priority="critical">
<instruction>
Destructive actions must show Dialog with specific action verb. See `writing-copy.md` for copy.
</instruction>
<full-example>
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle,
} from '@/components/ui/dialog';
<Dialog open={showDelete} onOpenChange={setShowDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete this report?</DialogTitle>
<DialogDescription>
This will permanently remove "{report.name}" and
all associated data. This cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="secondary"
onClick={() => setShowDelete(false)}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDelete}
>
Delete report
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</full-example>
<example type="incorrect">
{/* Yes/No, no description, wrong variant */}
<Dialog open={show}>
<DialogContent>
<DialogTitle>Confirm</DialogTitle>
<DialogDescription>Are you sure?</DialogDescription>
<DialogFooter>
<Button variant="secondary">No</Button>
<Button variant="default">Yes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</example>
</pattern>
</patterns>
<page-accessibility priority="critical">
<responsibility name="keyboard-navigation">
<instruction>
- Tab order follows visual reading order
- Every interactive element reachable via Tab
- No keyboard traps
- Skip-to-content link on pages with complex nav
</instruction>
<example type="correct">
<a href="#main-content" className="sr-only focus:not-sr-only
focus:absolute focus:top-4 focus:left-4 focus:z-50
focus:bg-background focus:px-4 focus:py-2 focus:rounded-md
focus:shadow-focus-ring">
Skip to main content
</a>
<Sidebar />
<main id="main-content">
<h1>Reports</h1>
{/* Content in logical tab order */}
</main>
</example>
</responsibility>
<responsibility name="heading-hierarchy">
<instruction>
Use heading levels (H1H6) 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.
</instruction>
</responsibility>
<responsibility name="image-alt-text">
<instruction>
Every image needs alt. Icons in buttons need aria-label.
</instruction>
<examples>
| 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 |
</examples>
<example type="correct">
{/* Icon + text: hide icon from screen reader */}
<Button variant="destructive">
<TrashIcon className="h-4 w-4 mr-2" aria-hidden="true" />
Delete report
</Button>
{/* Icon only: label on button */}
<Button variant="ghost" size="icon" aria-label="Delete report">
<TrashIcon className="h-4 w-4" />
</Button>
</example>
</responsibility>
<responsibility name="dynamic-content">
<examples>
| 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 |
</examples>
<example type="correct">
{/* Screen reader announcement for filtered results */}
<div aria-live="polite" className="sr-only">
{results.length} results found for "{query}"
</div>
<DataTable data={results} columns={columns} />
</example>
</responsibility>
<responsibility name="color-independence">
<instruction>
Never use color alone to convey meaning.
</instruction>
<example type="correct">
{/* Status with text + color */}
<span className="inline-flex items-center gap-1.5
bg-success text-success-foreground text-xs px-2.5 py-0.5
rounded-full">
<CheckCircle className="h-3 w-3" aria-hidden="true" />
Active
</span>
</example>
<example type="incorrect">
{/* Color only — invisible to colorblind users */}
<span className="h-2 w-2 rounded-full bg-success" />
</example>
</responsibility>
</page-accessibility>
<translation-and-testing priority="medium">
Short sentences and simple grammar translate more reliably. Plan for text expansion in localized UIs (e.g. German often adds 3040% 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.
</translation-and-testing>
<self-check>
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?
</self-check>
<edge-cases>
**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.
</edge-cases>