Best Practices
Dec 2025·6 min read

Building Accessible Component Systems (a11y) Without Sacrificing Visual Aesthetics

WAI-ARIA disclosure patterns, focus trap management in dialogs, spatial keyboard navigation contracts, live regions, and automated CI accessibility audits.

LEP
Lelianto Eko PradanaAI & Full Stack Engineer · Indonesia

Introduction

Web accessibility (a11y) is frequently treated as an afterthought or misconstrued as a constraint that forces visual designs to look plain. In reality, accessible applications are simply well-engineered applications that provide high usability for everyone—including users relying on screen readers, keyboard-only navigation, or voice control.

Here is how to craft custom component systems that deliver strong visual design and durable WCAG 2.1 AA conformance. Automated checks catch only a subset of issues; keyboard and screen-reader testing remain essential.


1. Keyboard Navigation Contracts & Focus Management

Every interactive UI component (modals, dropdowns, accordions, tabs) must support complete keyboard control.

Modal Dialog Focus Trap Example: When a modal opens: 1. Save the element that triggered the modal. 2. Move focus into the first focusable element inside the modal. 3. Trap Tab navigation within the modal boundary. 4. On pressing `Escape` or closing the modal, restore focus back to the original trigger element.

tsx
import { useEffect, useRef } from "react";

export function AccessibleModal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocusRef = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      previousFocusRef.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      previousFocusRef.current?.focus();
    }
  }, [isOpen]);

  if (!isOpen) return null;

  return (
    <div
      className="modal-backdrop"
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
      ref={modalRef}
      onKeyDown={(e) => e.key === "Escape" && onClose()}
    >
      <div className="modal-content">{children}</div>
    </div>
  );
}

2. Using Semantic WAI-ARIA Attributes

Do not invent custom non-standard attributes when native HTML5 semantics or ARIA roles exist:

  • Use <button> for actions and <a> for page navigation. Never use <div onClick={...}> without role="button" and keyboard handlers.
  • Add aria-expanded="true|false" to disclosure triggers (accordions, dropdown menus).
  • Use aria-live="polite" for dynamic status updates (such as form save notifications or live search results counters).

3. Automated Accessibility Testing in CI

Catch 40%+ of accessibility defects automatically before code reaches code review:

javascript
// Example Playwright + @axe-core/playwright automated test
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("Homepage should have no detectable accessibility violations", async ({ page }) => {
  await page.goto("http://localhost:3000/");

  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
    .analyze();

  expect(accessibilityScanResults.violations).toEqual([]);
});

4. Summary Checklist

  • [x] Visible focus rings (:focus-visible) preserved and styled harmoniously.
  • [x] Color contrast ratios meet at least 4.5:1 for standard text and 3:1 for large text/icons.
  • [x] All images have descriptive alt text (or alt="" for purely decorative images).
  • [x] Automated accessibility linting integrated via eslint-plugin-jsx-a11y.

5. Test the Experience, Not Just the Markup

Run a keyboard-only pass: every control should be reachable, the focus order should make sense, and focus should never disappear. Then test primary flows with VoiceOver or NVDA at least once. Verify zoom to 200%, reduced motion, error recovery, and touch targets on a real device.

Accessibility is a product-quality loop. Record defects as reusable component requirements so the next dialog, menu, or form inherits the fix instead of repeating the same audit finding.