← tasteMarkdown

States and accessibility

Condensed from: Open Design (https://github.com/nexu-io/open-design), Taste Skill (https://github.com/Leonxlnx/taste-skill), UI UX Pro Max (https://github.com/nextlevelbuilder/ui-ux-pro-max-skill), Hallmark (https://github.com/Nutlope/hallmark)

Target WCAG 2.2 AA. Shipping only the populated state is the commonest generated-UI failure.

Interaction states (every control)

State Spec
Hover One contrast-raising signal (colour step or 1px lift), 120-220ms, inside @media (hover:hover). No weight or layout change.
Focus :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px }, ≥3:1 vs adjacent colours, inset on inputs. Never outline:none without replacement; never animated.
Pressed Feedback within 100ms: darker fill or scale(.97), no layout shift.
Disabled Native disabled + opacity 0.5-0.6 + cursor:not-allowed + visible reason ("Add a card to publish"). Read-only looks different.
Loading (button) Keep width, spinner + label, block resubmit; announce via polite live region (not aria-busy on the button).
Selected Active fill like the nav pill; distinct from hover and focus.

Data states (every list, table, card, form, panel)

State Must contain
Loading Final layout, flat grey bars at real size (24px value → 24px bar), headers shown, no shimmer, no shift. Refresh keeps old values + freshness stamp.
Empty Keep frame and header; sentence + why + one action, spanning the block (grid-column: 1 / -1).
Error Inside the failing block, not full-screen: what failed, last good time, Retry. Input preserved.
Populated Real density, totals computed from rows.
Edge 200-char titles, missing avatar, 10,000 rows, long URLs (overflow-wrap:anywhere), +30-40% translated strings, RTL.

Loading thresholds:

Duration Indicator
0-300ms None
300ms-2s Skeleton or small spinner; once shown, keep ≥300ms (no flash)
2-10s Skeleton matching layout, or labelled spinner ("Loading payments…")
10-60s Determinate bar + Cancel; "Taking longer than expected" once at 15s
60s+ Stop animating; error with Retry/Cancel. Every request has a timeout.

Match error scope: field, form (summary + per-field), section (panel + Retry), page, app (banner). Retry immediately, then 2s, 4s, 8s; after 3 failures show "Contact support" + copyable error ID.

Forms

<label for="email">Email</label>
<input id="email" type="email" autocomplete="email" required
  aria-invalid="true" aria-describedby="email-hint email-error">
<span id="email-hint">Used for receipts only.</span>
<span id="email-error" role="alert">Email needs an @ and a domain.</span>

Touch targets

Bar Size
WCAG 2.5.8 AA (web) 24×24 CSS px (or 24px spacing circle)
WCAG 2.5.5 AAA 44×44 CSS px
iOS 44×44pt
Android 48×48dp

Keyboard and focus

Semantics

Reduced motion

Laws of UX, as numbers

Law Do
Fitts Primary targets big and near; thumb arc on mobile; plus the platform floors above.
Hick / choice overload 3-5 visible options per decision; pricing 3-4 tiers, exactly 1 recommended; settings ≤5 groups.
Jakob Keep category conventions (logo top-left links home, cart top-right, settings gear).
Miller / Cowan Working memory ~4 chunks: group fields under headings; persist filters, breadcrumbs.
Doherty Feedback under ~300ms (the popular "400ms" isn't in the 1982 paper); tap feedback ≤100ms.
Proximity 8-12px within groups, 32-48px between.
Common region Enclose only when proximity fails; padding ≥16px.
Von Restorff One item differs (recommended tier, active nav), with a non-colour cue too.
Serial position Key nav items first and last.
Peak-end Celebrate the end of a flow once; keep the middle calm.
Goal gradient "Step 2 of 4" with real progress; never fake pre-progress.
Postel Accept "555-0100", "5550100", "(555) 0100"; normalise; output one format.

RTL

Read by your agent through get_guide("states-accessibility") in the taste MCP.