Prototyping for Accessibility States
Designing and testing the full spectrum of interactive states — focus, error, disabled, loading, and more — is what separates accessible prototypes from beautiful but broken ones.
11 min read
The full lesson
Most prototypes show one happy path at one moment in time. A button looks pressed. A form looks filled. A modal looks open. What they rarely show is what happens when a keyboard user tabs through the interface, when a screen reader announces an error, or when a component sits in a disabled state mid-flow.
These omissions are not cosmetic. They represent a whole category of design decisions that developers will make by default — in the absence of any design guidance. Prototyping accessibility states explicitly is how design teams reclaim those decisions before they become production bugs.
Why Accessibility States Are a Prototyping Problem
Accessibility failures rarely start in code. They start in prototypes that show only one state.
When a Figma file has a single button variant with no documented focus ring, the developer implementing it has to invent the focus treatment. When an error state appears only as a spec comment and never as an actual component state, the result is usually a red border with generic “Invalid input” text — missing the correct color contrast, wrong location, and no aria-describedby connection to the input.
A complete prototype is not “a file that shows the design at its best.” It is a file that shows every state a user might encounter — including states that users relying on assistive technology will encounter exclusively. This is not an accessibility add-on. It is part of what “complete” means.
WCAG 2.2 — the current legal and technical baseline — added several criteria that directly address states: Focus Not Obscured (2.4.11 and the stricter 2.4.12), Target Size Minimum (2.5.8), and Accessible Authentication (3.3.8). None of these can be verified without showing interaction states in a prototype. Targeting WCAG 2.0 and treating focus styles as a developer afterthought is both technically outdated and legally indefensible in many jurisdictions.
The Full State Inventory
Before you can prototype accessibility states, you need to know what states exist for each component type. Here is the minimum state inventory for interactive components in a production prototype.
For all interactive elements (buttons, links, inputs):
- Default (resting)
- Hover
- Focus (keyboard focus — distinct from hover)
- Active (pressed / in-progress)
- Disabled
- Loading / pending (where applicable)
For form inputs specifically:
- Empty
- Filled (with valid content)
- Error (with inline validation message)
- Success / validated
- Disabled
- Read-only
For disclosure and overlay components (modals, tooltips, accordions):
- Closed / collapsed
- Open / expanded
- Focus-trapped state (for modals — where does focus land when it opens, and where does it return when it closes)
For dynamic content regions:
- Loading skeleton
- Loaded
- Empty state (zero results)
- Error state (fetch failure)
This list is not exhaustive for every component type, but it is the minimum set that must be designed before any handoff. Anything missing will be resolved inconsistently in production.
Designing Focus States That Pass WCAG 2.2
Focus Not Obscured (WCAG 2.2 criterion 2.4.11) requires that a focused component is not entirely hidden by sticky headers, floating elements, or overlapping content. The stricter AAA criterion 2.4.12 goes further: no part of the focus indicator can be obscured. In practice, this means focus ring placement and size must be verified against the actual layout, not designed in isolation.
The WCAG 2.2 minimum requirements for a compliant focus indicator are:
- An area of at least the perimeter of the unfocused component times 2 CSS pixels
- A contrast ratio of at least 3:1 between focused and unfocused states
- A contrast ratio of at least 3:1 against adjacent colors (background and component fill)
The modern approach pairs WCAG 2.2 compliance with APCA (Advanced Perceptual Contrast Algorithm) as a supplementary lens. WCAG 2.2 AA is the legal baseline. APCA helps you verify that the focus ring is visually salient in context — even when it technically passes the 3:1 ratio.
In practice, the most robust focus treatment uses a two-color outline: a solid ring in the brand’s high-contrast accent color, plus a 2px white (or near-white) gap between the ring and the element. This works on both light and dark surfaces. The white gap creates contrast against dark backgrounds; the colored ring creates contrast against light ones.
When using design tokens (named values that store a design decision, like a color or size, so the whole system stays in sync), focus ring values belong in the semantic token tier:
| Token | Role |
|---|---|
color.focus.ring | The primary focus indicator color |
color.focus.ring-offset | The gap between element and ring (usually white or near-black) |
size.focus.ring-width | Ring stroke width (typically 2–3px) |
size.focus.ring-offset | Gap width (typically 2px) |
Token-driven focus states follow the W3C DTCG stable JSON format — single-source semantic tokens that propagate to all platforms. Teams that hardcode focus colors in stylesheets instead of token references create exactly the drift between design and implementation that DTCG tokens are designed to prevent.
Prototyping Error and Validation States
Error states are a persistent blind spot in prototypes. The typical pattern is a happy-path form with a spec comment that says “add error state here.” What developers receive is a component with no designed error treatment. The result is usually a red border with generic “Invalid input” text — an implementation that fails WCAG 3.3.1 (Error Identification, which requires describing what is wrong) and WCAG 3.3.3 (Error Suggestion, which requires suggesting how to fix it when known).
Modern best practice for form validation prototyping:
- Validate on blur, not on submit. Show the error state as it actually appears during interaction — after the user leaves the field — not as a full-page error dump after form submission.
- Write the actual error message in the prototype. “Please enter a valid email address” is more useful than “Error message here.” Real copy reveals whether the message is long enough to break the layout and specific enough to be actionable.
- Show the
aria-describedbyrelationship visually. In the prototype annotation, mark the connection between the input and its error message. This maps directly to the accessible implementation. If it is not specified in the prototype, the relationship will not exist in the code. - Show both the input’s error treatment and the summary pattern. For multi-field forms, a summary at the top of the form listing all errors is required by WCAG 3.3.1. Prototype it.
Do
- Design distinct focus and hover states for every interactive component — they serve different users (keyboard vs. pointer) and must not be identical.
- Write real error messages in the prototype, specific to the error condition, so copy and layout are tested together.
- Annotate the
aria-describedbyassociation between inputs and their error messages in your handoff documentation. - Show the loading/pending state for any action that takes more than 0.3 seconds to resolve — use a skeleton or an explicit in-progress variant, not just a spinner overlay.
- Test focus order by manually tabbing through the prototype or by using Figma’s prototype mode and checking which element receives initial focus.
Don't
- Do not delegate focus ring design to developers — undocumented focus treatments become
outline: nonein production. - Do not use placeholder text as the only label for inputs; placeholder text disappears on focus, fails contrast requirements, and has no accessible label equivalent.
- Do not design only the error state for the first field in a form and call accessibility states complete.
- Do not skip the disabled state — show it visually distinct from the default with sufficient (not excessive) contrast reduction, and annotate whether it should receive focus.
- Do not use color as the only indicator of an error state; pair the color change with an icon, a label, and a message to satisfy WCAG 1.4.1 (Use of Color).
Disabled States: The Contrast Paradox
Disabled states are one of the most misunderstood areas in accessible prototyping. The common assumption is that disabled elements should look muted — low contrast, grayed out — to signal that they cannot be interacted with. That assumption is correct in principle.
The confusion comes from WCAG. Disabled UI components are exempt from the 4.5:1 contrast requirement (under criterion 1.4.3) precisely because they are not meant to be interacted with. The practical mistake is treating that exemption as a license to make disabled states so low-contrast that they become invisible — to everyone, including users who need to understand that a control exists but is currently unavailable.
A disabled state with a contrast ratio of 1.5:1 against its background is not accessible design. It is bad design hiding behind a WCAG exemption.
The modern calibrated approach:
- Target approximately 3:1 contrast for disabled states. This is low enough to signal non-interactivity compared to the active state (4.5:1 or higher), but high enough that the element remains perceptible.
- Document in the prototype whether the disabled element should receive keyboard focus. WCAG does not require disabled controls to be focusable, but some design systems make them focusable with a tooltip explaining why — a deliberate pattern that improves transparency, especially in forms where a control becomes enabled after a preceding step.
- Use OKLCH for computing disabled-state color variants. OKLCH is perceptually uniform, so a lightness reduction produces consistent perceived contrast changes across all hues. HSL-based darkening produces erratic results — the same HSL lightness delta that looks appropriately muted in blue may look nearly invisible in yellow.
Loading and Pending States
The loading state is the accessibility state most likely to be designed as an afterthought. The outdated habit is a generic spinner for every async operation. The modern approach uses skeleton screens for content-heavy layouts — so the user understands the shape of what is loading — and reserves explicit spinners or progress indicators for short blocking operations where a skeleton would be misleading.
From an accessibility prototyping standpoint, loading states require three things to be specified:
- Visual treatment — skeleton, spinner, or progress indicator, with appropriate color contrast. Skeletons in particular often fail contrast checks because they are designed for visual aesthetics rather than WCAG compliance.
- Announced state change — mark in the prototype annotation that this region uses
aria-live="polite"(oraria-busyduring loading) so screen reader users know when content loads without having to refocus manually. - Error fallback — every loading state needs a corresponding error state. A prototype that shows only “loading” and “loaded” is incomplete. “Failed to load” must be designed, including a recovery action.
Tooling: Figma Component States in Practice
In Figma, accessibility states map directly to component variants and interactive component properties. A well-structured button component in 2026 has variants for: Default, Hover, Focus, Active, Disabled, and Loading. Each variant is a separate component state, not a one-off frame. This structure matters because:
- Dev Mode exposes all variants when a developer inspects the component, giving them a visual specification for every state without hunting through multiple frames.
- Code Connect (Figma’s feature that links design components to their code counterparts) propagates state names into the connected Storybook stories, keeping design state names and code state names synchronized.
- Variable-driven theming means that a focus ring defined as a token reference in the Figma component automatically updates when dark mode token values are applied — no separate dark-mode state design is required if the tokens are correctly structured.
The outdated workflow: designing states as separate frames in a flat artboard layout, with redline annotations exported to a PDF. The modern workflow: component variants with token bindings, inspectable in Dev Mode, linked to Storybook via Code Connect, with state transitions documented in prototype connections where applicable.
For testing state behavior beyond Figma’s prototype capabilities — particularly keyboard navigation order and screen reader announcements — code-based hi-fi prototyping in Storybook is the gold standard. A Storybook story with @storybook/addon-a11y running the axe accessibility engine gives real-time WCAG violation feedback against the actual implemented states, closing the loop between design intent and code reality.
Handoff: What Developers Need to See
A prototype that documents accessibility states is only useful if that documentation survives the handoff. The minimum handoff requirements for accessible state documentation are:
- All component variants visible in Figma Dev Mode, with accurate state names matching the code implementation
- Annotation of ARIA roles, properties, and relationships (
aria-label,aria-describedby,aria-expanded,aria-live) on any component whose behavior cannot be inferred from visual inspection alone - Token names for all color and size values used in states — not hardcoded hex or px values
- Explicit notes on focus management for modals, drawers, and any flow where focus must be programmatically moved
- Error message copy finalized and co-located with the error state variant — not buried in a separate content document
Teams using Storybook as a living design system can co-locate this documentation as story-level annotations. Teams working primarily in Figma should use the Dev Mode annotation layer. Redline PDFs and separate spec documents are not an adequate substitute — they drift from the design file within weeks and are reliably out of date by the time implementation begins.