How to Build Accessible UI Components That Meet WCAG 2.2 Across Every State
Build UI components that are accessible by design, with WCAG 2.2 compliant focus states, keyboard navigation, ARIA labels, and contrast across every interactive state.
The One Number That Defines Production-Ready UI Components
A UI component is production-ready only when it passes the 24×24 CSS pixel target-size threshold defined in WCAG 2.2 Success Criterion 2.5.8. That single number, 24 pixels square, is the minimum area for any pointer-accessible target, and it is the most commonly failed requirement in audited interfaces. Every interactive element that does not meet it, or that does not have at least 24 CSS pixels of spacing between its edge and an adjacent target, is a legal liability in any jurisdiction that enforces WCAG 2.2 AA, including the United States under ADA Title II, the European Union under EN 301 549, and the United Kingdom under the Equality Act 2010. The standard is not a guideline; it is the floor. The rest of accessible UI component design WCAG compliance is built on this same principle: measurable thresholds, no exceptions for aesthetics.
Focus State Design: Minimum Area and Minimum Contrast
What WCAG 2.2 Requires of a Focus Indicator
WCAG 2.2 Success Criterion 2.4.11 Focus Appearance replaces the vague requirement for a focus indicator with two precise measurements. The focus indicator must be at least as large as the area of a 2 CSS pixel thick perimeter around the unfocused component. If a button is 40×30 pixels, its perimeter area is 2 × (40 + 30) = 140 square pixels, and the focus indicator must cover at least that area. The indicator must also maintain a contrast ratio of at least 3:1 against the adjacent colours in the focused state.
Why Default Browser Rings Fail
A default browser focus ring, a thin 1-pixel dotted outline in a colour that may or may not stand out against the surroundings, almost always fails both requirements. The ring is too thin, 1 pixel instead of the required 2-pixel equivalent area. Its colour is a system default that does not pass the 3:1 contrast threshold against the component's surface. To pass, use a 2-pixel solid outline in a colour that achieves at least 3:1 contrast against both the component's unfocused surface and the page background. The outline must be present when the component is focused, not only when it is hovered. Focus state is a keyboard-only affordance; hover state is pointer-only. The two must be visually distinct.
Keyboard Navigation: Tab Order, No Keyboard Trap, and Skip Links
Every interactive component must be reachable and operable using only the keyboard. Tab order must follow the visual reading order and preserve meaning, as required by WCAG 2.2 Success Criterion 2.4.3. The focus must not be trapped in any component; the user must be able to move focus away using standard keyboard methods (Tab, Shift+Tab, arrow keys). This is WCAG 2.2 Success Criterion 2.1.2 No Keyboard Trap. A modal dialog that traps focus inside itself without providing an Escape key handler is a failure. A carousel that advances on arrow keys but does not let the user Tab out of the active slide is a failure.
Skip Links That Actually Work
A skip link, which allows a keyboard user to bypass repeated navigation blocks, is required by WCAG 2.2 Success Criterion 2.4.1 Bypass Blocks. The skip link must be the first focusable element on the page, and it must become apparent on focus. A skip link that is present in the HTML but hidden with display:none is not a skip link; it is a compliance checkbox that fails a real user.
ARIA Labels for Interactive Elements: Name, Role, Value, and the Label in Name Rule
ARIA does not make a component accessible. Correct HTML structure with ARIA as a supplement does. Every interactive element must have an accessible name that is programmatically determinable, as required by WCAG 2.2 Success Criterion 4.1.2 Name, Role, Value. The name must contain the visible label text, per WCAG 2.2 Success Criterion 2.5.3 Label in Name. A button that displays 'Submit' but has aria-label='Send Form' fails because the accessible name does not begin with the visible label. Use aria-labelledby to point to a visible label element when possible; use aria-label only when no visible label exists. Use aria-describedby to provide additional instructions, such as a password format requirement.
Why the Native Button Wins
The role must match the native HTML element's implicit role. A div with role='button' is always worse than a native button element, because the native button is focusable, activatable with Enter and Space, and exposed to assistive technology without any ARIA. The native button element is the single most important ARIA-avoidance technique in accessible UI design.
Component Contrast Requirements: Non-Text Contrast at 3:1
WCAG 2.2 Success Criterion 1.4.11 Non-Text Contrast requires a contrast ratio of at least 3:1 for UI component boundaries and states. This applies to the border of a button, the fill of a selected tab, the tick of a checkbox, the surface of a focus indicator, and the border of an input field in its error state. The ratio is measured between the component's boundary colour and the adjacent colour behind it. A button with a 1-pixel grey border on a white field that measures 2.8:1 fails. A selected tab with a blue underline that measures 2.5:1 against the tab's surface fails. The exemption for disabled UI components means a disabled button with low-contrast text is acceptable, but an enabled button with the same contrast is a failure.
Measure from Hex Values, Not Screenshots
The contrast calculation uses the relative luminance formula defined in WCAG 2.2: L = 0.2126 × R + 0.7152 × G + 0.0722 × B, where R, G, B are linearised sRGB values. A contrast-checker plugin that samples antialiased edge pixels can inflate the ratio by up to 1.5:1, so verify the ratio by measuring the intended foreground and background colours from their hex or RGB values, not from a screenshot sample.
| Requirement | WCAG 2.2 Success Criterion | Threshold | Applies To |
|---|---|---|---|
| Normal text contrast | 1.4.3 Contrast Minimum | 4.5:1 | Text and images of text under 18pt or 14pt bold |
| Large text contrast | 1.4.3 Contrast Minimum | 3:1 | Text at least 18pt or 14pt bold |
| Enhanced normal text contrast | 1.4.6 Contrast Enhanced (AAA) | 7:1 | Text and images of text under 18pt or 14pt bold |
| Enhanced large text contrast | 1.4.6 Contrast Enhanced (AAA) | 4.5:1 | Text at least 18pt or 14pt bold |
| Non-text contrast | 1.4.11 Non-Text Contrast | 3:1 | UI component boundaries, states, graphical objects |
| Focus indicator area | 2.4.11 Focus Appearance | Area of 2 CSS px thick perimeter | Keyboard focus indicator |
| Focus indicator contrast | 2.4.11 Focus Appearance | 3:1 against adjacent colours | Keyboard focus indicator |
| Target size | 2.5.8 Target Size | 24×24 CSS pixels | Pointer targets (exempts inline links) |
| Target spacing | 2.5.8 Target Size | 24 CSS px between adjacent targets | Undersized targets with spacing alternative |
Worked Example: Custom Select Menu Failure and ARIA Listbox Pattern Fix
A custom-styled select menu that hides the native select element and renders a div with role='listbox' is a common failure. Without a keyboard handler for arrow keys and Enter, the component is unreachable by keyboard. Without aria-label or aria-labelledby pointing to a visible label, it is invisible to a screen reader. Without role='listbox' on the container and role='option' on each item, the screen reader does not announce the options or their selection state. This component fails WCAG 2.2 Success Criteria 2.1.1 Keyboard, 4.1.2 Name Role Value, and 2.4.7 Focus Visible simultaneously.
The fix starts with the native HTML button element. Wrap it in a container with role='combobox' and aria-expanded. Use aria-haspopup='listbox'. The list of options uses role='listbox' on the container and role='option' on each item. Each option has aria-selected set to true or false. The currently focused option is tracked with aria-activedescendant on the combobox. Keyboard handlers for arrow keys, Enter, Escape, and Tab are added. The visible label is linked to the combobox with aria-labelledby. The selected value is announced by a live region with role='status'. The focus indicator on the button meets the 2-pixel perimeter and 3:1 contrast requirements. This pattern is documented in the W3C ARIA Authoring Practices Guide, and it is the only reliable way to build a custom select menu that passes an accessibility audit.
Error State Identification, Screen Reader Announcements, and Live Regions
WCAG 2.2 Success Criterion 3.3.1 Error Identification requires that an input error be described in text and that the item in error be identified. A red border alone is not sufficient; colour must not be the only visual means of conveying information, per WCAG 2.2 Success Criterion 1.4.1 Use of Colour. Add an error message text that describes the problem and links the error to the input field using aria-describedby. Use aria-invalid='true' on the input field to programmatically indicate the error state.
Getting the Error Announced
The error message must be announced by a screen reader without moving focus, which requires a live region. Use role='alert' for a message that interrupts the user, or role='status' for a message that does not. The error state boundary must meet the 3:1 non-text contrast requirement against the adjacent background. A red border that measures 2.5:1 against a white background is a contrast failure. Use a darker red or a thicker border to meet the threshold.
Common Questions
What is the minimum focus indicator area required by WCAG 2.2?
WCAG 2.2 Success Criterion 2.4.11 Focus Appearance requires the focus indicator area to be at least as large as the area of a 2 CSS pixel thick perimeter around the unfocused component. For a 40×30 pixel button, the perimeter area is 2 × (40 + 30) = 140 square pixels, and the focus indicator must cover at least that area.
Does a default browser focus ring pass WCAG 2.2?
Almost never. The default ring is 1 pixel thick, which fails the 2-pixel equivalent area requirement. Its colour is a system default that does not achieve the required 3:1 contrast against the component's surface.
What contrast ratio does WCAG 2.2 require for non-text UI components?
WCAG 2.2 Success Criterion 1.4.11 Non-Text Contrast requires a contrast ratio of at least 3:1 for UI component boundaries and states, including borders, fills, focus indicators, and error indicators.
What is the minimum touch target size in WCAG 2.2?
WCAG 2.2 Success Criterion 2.5.8 Target Size requires a minimum of 24×24 CSS pixels for pointer targets. Inline links are exempt. An alternative is to have at least 24 CSS pixels of spacing between adjacent targets that are smaller than the minimum.
What is the Label in Name rule and why does it matter?
WCAG 2.2 Success Criterion 2.5.3 Label in Name requires that the accessible name of a UI component with a visible text label must contain that visible text. A button that displays 'Submit' but has an aria-label of 'Send Form' fails because the accessible name does not begin with the visible label. Screen reader users rely on the name matching the label to confirm they are on the correct control.