`. A real link must support Cmd/Ctrl/middle-click. See [semantics-and-aria.md](semantics-and-aria.md) for landmarks and button-vs-link.
## Disabled means unavailable
Use native `disabled` when a control is genuinely unavailable. Reach for `aria-disabled="true"` only when it should stay focusable, then block pointer, keyboard and form behavior in code and style the state explicitly. The rules are in [semantics-and-aria.md](semantics-and-aria.md).
## Visible focus rings
Style `:focus-visible`, not bare `:focus`. Keyboard users get a ring and mouse users usually don't. Prefer the browser's unmodified indicator.
A custom ring needs a project focus token or another explicit color. It must reach 3:1 against every adjacent color it crosses, `currentColor` included, under 1.4.11. A `2px` solid perimeter with a 3:1 change of contrast is the 2.4.13 AAA target. Never use `outline: none` without a verified replacement, and preserve system colors in forced-colors mode.
A focused element must never sit fully hidden behind a sticky header, footer or banner (2.4.11). Pad the scroller by the sticky chrome's height, a fix `better-layout` owns. Recipes are in [focus-and-keyboard.md](focus-and-keyboard.md).
## Full keyboard support
Every pointer interaction needs a keyboard path. Follow the ARIA APG patterns: Escape closes overlays, arrow keys move within composite widgets, Tab moves between widgets, Enter and Space activate buttons.
Use only `tabindex="0"` to join the natural tab order and `tabindex="-1"` for programmatic focus. Positive values break that order. Composite widgets use roving tabindex, where the active item is `0` and every other is `-1`. Where focus must stay in an input, as in a combobox, use `aria-activedescendant` instead. Key maps per widget are in [focus-and-keyboard.md](focus-and-keyboard.md).
## Trap and restore focus
Prefer `
` opened with `showModal()`, which makes the background inert and handles Escape. A custom overlay sets `inert` on the background, `role="dialog"` and `aria-modal="true"` on itself. Either way, move focus inside on open and return it to the trigger on close.
Client-side route changes reset nothing. Update `document.title` and move focus to the new view's `` or ``. Both recipes are in [focus-and-keyboard.md](focus-and-keyboard.md).
## Minimum hit area
WCAG 2.5.8's Level AA baseline is a 24×24 CSS-pixel target, or one of its exceptions. Aim for 44×44px on touch and 40×40px on desktop where density permits. Extend with a pseudo-element when the visible element should stay smaller.
Never let extended hit areas overlap. Give decorative layers `pointer-events: none`, so a glow never swallows the clicks meant for the control beneath it.
Every drag interaction needs a single-pointer alternative, such as buttons or a menu that reorder or move the same item (2.5.7). Sizes, exceptions and collision rules are in [hit-areas.md](hit-areas.md).
## Label and type every control
Every input gets a `` or a wrapping ``. A placeholder is never a label. Label and control share one hit target, with no dead zone between a checkbox and its text.
Add a valid `autocomplete` token and a descriptive `name` attribute, plus the `type` and `inputmode` that summon the right keyboard. Never block paste; users paste passwords and one-time codes. See [forms.md](forms.md).
## Errors that announce
Validate on submit, never by disabling submit until the form is valid. Mark failing fields `aria-invalid="true"`, point `aria-describedby` at the inline error text and focus the first invalid field. Submit behavior while the request runs is in [forms.md](forms.md).
## Accessible names everywhere
Icon-only buttons need a descriptive `aria-label`. Visible label text must appear in the accessible name. Decorative elements get `aria-hidden="true"`, never on a focusable element.
## Pair color with a second cue
Status needs a redundant cue: an icon, text or an underline alongside the color (1.4.1).
This skill decides which contrast requirement applies:
| Criterion | Applies to | Minimum |
| --- | --- | --- |
| 1.4.3 AA | Text | 4.5:1 |
| 1.4.3 AA | Large text, at least `24px` or `18.67px` bold | 3:1 |
| 1.4.11 AA | UI component boundaries, states and meaningful graphics | 3:1 |
Inactive controls and logos are exempt. Use `better-colors` to measure the rendered pair. When it fails, report the pair and the criterion it misses, and leave the colors alone unless asked.
## Honor prefers-reduced-motion
Wrap motion in `@media (prefers-reduced-motion: no-preference)` so it is opt-in. Under reduced motion, replace slides and scales with opacity cross-fades, and kill parallax and autoplay entirely. See [motion-and-zoom.md](motion-and-zoom.md).
## Nothing the user needs runs on a timer
Anything moving, blinking or updating on its own for more than 5 seconds needs a visible pause control (2.2.2). Toasts carrying an action or an error stay until dismissed. The rules are in [motion-and-zoom.md](motion-and-zoom.md).
## Announce dynamic content
Three mechanisms, three jobs. `aria-describedby` carries field-specific validation. A polite live region (`role="status"`) carries non-urgent updates not tied to a control, such as toasts and result counts. `role="alert"` carries urgent untied errors and nothing else.
Repeated polite announcements need a stable empty region rendered before its text updates. Dynamically inserted alerts vary in support, so test them on the screen readers you target. See [screen-readers.md](screen-readers.md).
## Alt text by purpose
Decorative images get `alt=""`. Informative images describe the meaning. Functional images describe the action, never the picture. The full table is in [screen-readers.md](screen-readers.md).
## Structure is navigation
Use headings that describe their sections and form a coherent outline. Expose one visible primary `` landmark. When repeated navigation or chrome precedes it, make a "Skip to content" link the first focusable element. Anchored headings get `scroll-margin-block-start`.
## Survive zoom and text resize
Text must survive 200% resize (1.4.4), and the page must reflow at 320px width without horizontal scrolling (1.4.10). Text containers take `min-height`, not `height`, and `better-layout` owns the rest of that fix. Never set `maximum-scale=1` or `user-scalable=no` in the viewport meta. iOS input zoom is fixed with `better-typography`'s `16px` rule instead.
## Before you finish
| Pattern | Fix |
| --- | --- |
| `outline: none` or `outline-none` with no `focus-visible` replacement nearby | Restore the browser ring or add a verified custom one |
| `