Why components are leverage—for good or for harm
Modern teams ship experiences via shared components: buttons, menus, dialogs, tabs, carousels, inputs, and date pickers. When those components embody good semantics, keyboard support, and readable contrast, accessibility scales “for free.” When they don’t, each reuse reproduces the same barriers— unlabeled icons, invisible focus, keyboard traps—across the entire product. The problem isn’t a single page; it’s a systemic defect.
Common component failure patterns
1) Div-as-button and fake links
A styled <div> looks like a button but lacks semantics and keyboard behavior. Likewise, a “link” that isn’t an <a href> breaks navigation.
Prefer native elements. If custom rendering is needed, ensure the full behavior:
<!-- Real button with keyboard + semantics -->
<button type="button" class="btn">Apply filters</button>
<!-- If absolutely custom: provide role, name, and key handlers -->
<div role="button" tabindex="0" aria-pressed="false"
onkeydown="if(event.key==='Enter'||event.key===' ') this.click()">Toggle</div>
2) Invisible or removed focus
Global CSS that nukes outline removes orientation for keyboard users. Your primitives should define a focus token and use it consistently.
/* Provide a strong, consistent focus style */
:focus-visible {
outline: 3px solid #1a73e8; /* use a token from your design system */
outline-offset: 3px;
border-radius: 4px;
}
3) Dialogs and off-canvas panels without focus management
Proper dialogs must: move focus into the dialog on open, trap focus inside, label with a title announced to assistive tech, and return focus to the trigger when closed. Many modal libraries miss one or more of these steps, producing keyboard traps or “lost focus” behind the overlay.
// Conceptual focus trap
const focusables = () => dialog.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])');
dialog.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
const items = [...focusables()];
const first = items[0], last = items[items.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});
4) Menus, comboboxes, and autocomplete without roving tabindex
Arrow key navigation is expected inside menus and lists; focus should “rove” with a single tabindex="0" item at a time, others at -1,
and Esc should close and return focus. Without this, keyboard users cannot select items reliably.
5) Tabs and accordions missing roles, states, and relationships
Tabs require role="tablist", role="tab", aria-selected, and an aria-controls link to the panel.
Accordions expect keyboard toggling with Enter/Space and an adjacent panel in DOM order for coherent reading.
6) Icon-only buttons with no accessible name
“Close,” “Search,” “Cart,” and “Filter” icons must expose a name via text content, aria-label, or an associated label. Tooltips alone don’t count.
7) Sliders, date pickers, and carousels that can’t be operated by keyboard
Sliders should respond to arrow keys and announce values. Carousels need focusable controls and a pause button. Calendars should allow direct text entry and predictable arrow navigation with month/year announcements.
8) Color and contrast tokens that fail in some states
It’s not enough that default states pass. Hover, active, disabled, error, and focus must remain readable across light/dark themes and layered surfaces.
Retrofitting a design system without breaking everything
Treat accessibility as a versioned upgrade path. Don’t sprinkle fixes page by page; fix the primitives and propagate.
- Inventory the library: List primitives (Button, Link, Input, Select, Checkbox/Radio, Textarea, Menu, Tabs, Dialog, Alert, Tooltip, Toast, Carousel, DatePicker).
- Define acceptance criteria per component: role, name, state, keyboard map, focus behavior, contrast tokens, error messaging.
- Create an “a11y-hardening” branch: Upgrade components behind feature flags if needed; publish release notes with breaking changes.
- Ship codemods: Where APIs change (e.g.,
<IconButton label="Close">now required), provide automated code transforms. - Migrate high-risk flows first: Navigation, login, checkout, account management, and any regulated tasks.
What “done” looks like for key primitives
Button & Link
- Button is a real
<button>with visible focus and a clear label. Disabled state announced and visually distinct. - Link is a real
<a href>for navigation; maintains underline or another non-color cue, with an obvious visited state.
Dialog
- Focus sent to first interactive element on open; trapped until closed; returns to trigger.
- Announced with a title (
aria-labelledby) and properrole="dialog"orrole="alertdialog"for blocking flows. - Background inert (no tabbing behind). Esc closes unless destructive.
Menu / Combobox
- Arrow navigation; Enter/Space selects; Esc closes; focus returns to trigger.
- Exposes current selection and count to assistive tech; announces filtering results.
Tabs
role="tablist"; each tab hasrole="tab",aria-selected, and controls anid-tied panel.- Left/right arrows switch tabs; Tab moves into the active panel.
Form inputs
- Persistent labels (
<label for>), hints viaaria-describedby, specific error text announced via live regions. - Mobile-friendly types and
autocompletetokens; 44px tap targets; no zoom blocking.
Carousel
- Pause/prev/next buttons in the tab order with visible focus; no auto-rotate without a pause control.
- Screen reader announcements for slide changes via a polite live region.
Governance: keep the library accessible as it evolves
- Storybook a11y panel: Add automated checks and interactive keyboard stories (e.g., “Tab through dialog,” “Arrow through tabs”).
- Unit tests for roles/names/states: Assert that core components expose expected attributes and respond to keys.
- Visual regression for focus: Snapshot focus rings/states so design tweaks cannot erase them silently.
- Lint rules: Disallow
onClickon non-interactive elements, enforce<a href>for navigation, and block removal of outlines without replacement. - Contrast CI: Token-based tests to ensure all states in light/dark themes meet readability targets.
- Change control: Any component touching roles/keys/focus requires a11y sign-off and release notes.
Documentation your teams will actually use
- One-page specs per component: role, name, state matrix; keyboard map; focus behavior; examples; anti-patterns.
- Copy/paste snippets: Developers ship faster when they can paste correct markup and JS handlers.
- Design tokens with examples: Show contrast-safe pairs and a single focus style applied to varied controls.
- “Red flag” gallery: Screenshots of common mistakes in your product with the correct pattern beside them.
Auditing at scale: find systemic defects quickly
- DOM sampling: Query production pages for suspicious patterns (
div[role="button"]:not([tabindex]),img[alt=""]used for icons, etc.). - Keyboard traces: Instrument key events in critical flows to detect unreachable CTAs or lost focus.
- Error analytics: Track form error strings and loops; spikes often indicate missing labels or weak validation copy.
- Assistive tech smoke tests: NVDA/JAWS/VoiceOver quick passes on each primitive after changes.
Migration playbook: reduce risk while upgrading
- Shadow components: Introduce accessible versions under new names (
DialogV2,TablistV2) and migrate feature by feature. - Adapter layer: Provide compatibility props so legacy code compiles while deprecations warn in dev.
- Flag high-traffic surfaces: Roll out to nav, product pages, checkout, and account first; measure completion rate and error reductions.
- Retire old APIs: Set deadlines, publish codemods, and remove legacy exports to prevent backsliding.
Evidence to capture when a component causes harm
If a shared widget prevents access—e.g., a modal that traps focus or a button without a label—document it so impact can be proven:
- Screenshot or short recording of the component behavior and attempted keyboard actions.
- Exact steps, the URL, date/time, and the assistive tech/browser mix if applicable.
- Consequence (unable to checkout, missed booking, can’t submit medical form, lost discount).
- Scope note: list other pages using the same component to show the systemic nature.
Why fixing the library is good business
Repairing primitives reduces abandonment, support load, and future rework. When components are accessible by default, every new page ships with lower risk and higher conversion. It’s the rare investment that improves user experience, legal posture, and engineering velocity at once.
How The Brensilber Law Firm helps (briefly)
When inaccessible components deny equal access to services, we help turn evidence into action—focusing on clear documentation, impact, and outcomes that drive meaningful fixes. If you encountered barriers like those described here, contact us to discuss options, or explore our Resource Hub for more guides.