accessibility

why

a screen speaks through two channels: pixels for sighted pointer visitors, the element tree for keyboard and screen reader visitors. when the channels diverge, part of the audience gets a different product — an unreachable action, an unannounced loading state, a control named by an emoji. retrofitting the element tree after visual design settles is where divergences are created; building from real semantic elements makes the channels agree by construction. one implementation serves every visitor, and each visible state's truthfulness extends to what assistive technology hears.

rule

accessibility is not a final pass; it is a property of the semantic state surface. real elements with real names, keyboard paths to every action, visible focus, honest announcements — the truthfulness of truthful product states applied to what a screen reader or keyboard-only visitor experiences.

the floor

concernstandard
interactive elementsreal <button>, <a>, <input> with an accessible name — never a clickable div.
imagesalt stating the fact the image carries; decorative images empty alt.
focusvisible focus ring on every interactive element, in the order a keyboard reaches them.
announcementsloading, empty, blocked, failure, and ready surfaces announce via aria-live polite regions.
skeletonsreserved geometry, subtle motion, exposed as loading state to AT — not read as content, per skeleton loading.
motionrespect prefers-reduced-motion; transitions are decoration, not information.
contrasttext on surfaces meets the token system's contrast floor.
formslabels bound to inputs, errors linked to their field, per form validation.

decision test

before shipping, answer:

  • can a keyboard-only visitor reach and complete every action?
  • does a screen reader hear the same states a sighted visitor sees — loading as loading, empty as empty, blocked as blocked?
  • does any visible state fail to announce?

any "no" means the surface is not done.

do not

  • emoji or icon as the only name of a control.
  • disabling or removing the focus ring because it is loud — restyle it within the token system.
  • announcing fake progress ("loading done") or hiding a failure state from AT.
  • color alone carrying a semantic difference.
  • aria-* as decoration — each attribute must have its real purpose.

verification

  • keyboard the full flow: sensible tab order, visible focus at every stop, no keyboard trap.
  • screen reader through each semantic state (pending, empty, blocked, recoverable failure, ready): announcements match the visible state one-to-one.
  • automated audit (axe) on the ready state as a floor-check, not a substitute for the manual pass.

good and bad

bad: unreachable, unnamed control

<div onClick={saveWorkflow}>💾</div>

no tab stop, no enter/space handler — a keyboard-only visitor cannot reach it; a screen reader announces nothing or reads the emoji as decoration; the save never happens; voice control cannot say "save workflow" because the element has no accessible name.

bad: silent state change

<WorkflowEditor>
{isLoading && <div role="status">processing...</div>}
<WorkflowDisplay>...</WorkflowDisplay>
</WorkflowEditor>

the status div is not aria-live, so loading is silent to AT; a keyboard user does not know the save is in flight and may close the editor or edit mid-save.

good: native element

<button type="button" onClick={saveWorkflow}>
save workflow
</button>

keyboard semantics, visible focus ring, visible label naming the action. one implementation serves pointer, keyboard, and screen reader visitors — no retrofit, no divergence between visible state and AT announcements.

good: explicit announcement

<WorkflowEditor>
<div role="status" aria-live="polite" aria-atomic="true">
{isLoading ? "saving workflow..." : "ready"}
</div>
<WorkflowDisplay>...</WorkflowDisplay>
</WorkflowEditor>

screen readers announce "saving workflow..." immediately; keyboard users see focus on the status region; the state is truthful to everyone.

search pages

go to any page