Knowledge

Web Accessibility (a11y) as Engineering Practice in 2026

Beyond compliance checklists—integrating accessibility into your engineering workflow to build inclusive products.

3/12/20267 min readKnowledge
Web Accessibility (a11y) as Engineering Practice in 2026

Executive summary

Beyond compliance checklists—integrating accessibility into your engineering workflow to build inclusive products.

Last updated: 3/12/2026

The business case for accessibility

Web accessibility is frequently treated as a compliance checkbox—something to "fix" before a regulatory audit or after receiving a user complaint. This approach is fundamentally flawed for three reasons:

  1. Market opportunity: Over 1 billion people worldwide live with disabilities. Inaccessible products exclude 15% of the global population.
  1. Legal risk: WCAG 2.2 Level AA is increasingly required by law. The European Accessibility Act (EAA) mandates compliance for public sector websites by 2025 and for private sector by 2026. The US DOJ has clarified that the ADA applies to websites and apps.
  1. Technical debt: Treating accessibility as an afterthought creates accumulated technical debt. Retrofitting accessibility into existing applications is exponentially more expensive than building it in from the start.

The mature engineering approach is different: integrate accessibility into your development workflow, automated testing, and design system from day one. Accessibility becomes a quality attribute like performance or security, not a separate concern.

WCAG 2.2: The technical standard

WCAG 2.2 (Web Content Accessibility Guidelines) provides four principles organized as an acronym: POUR.

Perceivable

Content must be presented in ways users can perceive:

  • Text alternatives: Provide alt text for images, captions for videos, and labels for form inputs
  • Time-based media: Provide alternatives for time-based content (captions, transcripts)
  • Adaptable: Content can be presented in different ways without losing meaning
  • Distinguishable: Make it easy to see and hear content

Operable

User interface components and navigation must be operable:

  • Keyboard accessible: All functionality must be available via keyboard
  • Enough time: Provide users enough time to read and use content
  • Seizures: Do not design content that causes seizures
  • Navigable: Help users navigate and find content

Understandable

Information and user interface operation must be understandable:

  • Readable: Make text content readable and understandable
  • Predictable: Make Web pages appear and operate in predictable ways
  • Input assistance: Help users avoid and correct mistakes

Robust

Content must be robust enough to be interpreted by a wide variety of user agents, including assistive technologies:

  • Compatible: Maximize compatibility with current and future user agents

Semantic HTML: The foundation

Semantic HTML provides the foundation for accessibility. Use elements for their intended purpose:

html<!-- Bad: Non-semantic structure -->
<div class="header">Navigation</div>
<div class="button">Click me</div>
<div class="form">
  <div class="input">Email</div>
</div>

<!-- Good: Semantic HTML -->
<header>
  <nav>Navigation</nav>
</header>
<button>Click me</button>
<form>
  <label for="email">Email</label>
  <input id="email" type="email" />
</form>

Critical semantic elements

ElementPurposeAccessibility Impact
<nav>Navigation sectionsScreen reader users can skip to navigation
<main>Main contentScreen reader users can skip to main content
<article>Self-contained contentDefines independent content units
<section>Thematic groupingOrganizes content hierarchically
<aside>Tangentially related contentSeparates supplementary information
<button>Action triggersKeyboard accessible by default
<input type="text">Text inputAutomatically associated with labels

ARIA landmarks

ARIA landmarks help screen reader users navigate:

html<div role="navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</div>

<!-- Better: Use native elements where possible -->
<nav>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

Keyboard navigation

All interactive elements must be keyboard accessible by default:

typescript// Bad: Keyboard event trapping on elements that aren't focusable
<div onClick={handleClick}>Click me</div>

// Good: Use semantic interactive elements
<button onClick={handleClick}>Click me</button>

// For custom interactive elements, add keyboard support
function CustomButton({ onClick, children }: ButtonProps) {
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' || e.key === ' ') {
      onClick();
    }
  };

  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onClick}
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

Focus management

typescript// Manage focus for modals
function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = React.useRef<HTMLDivElement>(null);

  React.useEffect(() => {
    if (isOpen && modalRef.current) {
      // Focus the modal when opened
      modalRef.current.focus();
    }
  }, [isOpen]);

  // Trap focus within modal
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Tab') {
      const focusableElements = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );

      const firstElement = focusableElements?.[0] as HTMLElement;
      const lastElement = focusableElements?.[
        focusableElements.length - 1
      ] as HTMLElement;

      if (e.shiftKey) {
        if (document.activeElement === firstElement) {
          e.preventDefault();
          lastElement?.focus();
        }
      } else {
        if (document.activeElement === lastElement) {
          e.preventDefault();
          firstElement?.focus();
        }
      }
    }
  };

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

Visible focus indicator

css/* Always show focus state */
:focus-visible {
  outline: 2px solid #2563EB;
  outline-offset: 2px;
}

/* Only show focus when keyboard is used (modern browsers) */
:focus:not(:focus-visible) {
  outline: none;
}

Screen reader support

Proper labeling

html<!-- Bad: Unlabeled input -->
<input type="text" />

<!-- Good: Explicitly associated label -->
<label for="email">Email</label>
<input id="email" type="text" />

<!-- Also good: Inline label -->
<label>
  Email
  <input type="text" />
</label>

ARIA labels for complex elements

typescript// Icon buttons need labels
<IconButton
  icon={<XIcon />}
  aria-label="Close dialog"
  onClick={onClose}
/>

// Status updates need live regions
<div role="status" aria-live="polite">
  {statusMessage}
</div>

// Dynamic content announcements
<div role="alert" aria-live="assertive">
  {errorMessage}
</div>

Avoid "click here" and similar links

html<!-- Bad: Non-descriptive -->
<a href="/about">Click here to learn more</a>

<!-- Good: Descriptive -->
<a href="/about">Learn more about us</a>

Color contrast and visual design

WCAG 2.2 Level AA requires:

  • Normal text: 4.5:1 contrast ratio
  • Large text (18pt+ or 14pt+ bold): 3:1 contrast ratio
  • UI components: 3:1 contrast ratio

Testing contrast ratios

Use automated tools to verify contrast:

typescript// Using color-contrast package
import { checkContrast } from 'color-contrast';

const result = checkContrast('#FFFFFF', '#2563EB');

if (result.ratio < 4.5) {
  console.warn('Contrast ratio below WCAG AA threshold');
}

Don't rely on color alone

html<!-- Bad: Color only indicates error -->
<div style="color: red">Error: Invalid email</div>

<!-- Good: Additional indicators -->
<div className="text-red-600" role="alert">
  <span aria-hidden="true">❌</span>
  Error: Invalid email
</div>

Accessibility in your design system

Integrate accessibility into your component library:

typescript// Accessible button component
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  icon?: React.ReactNode;
}

export function Button({
  variant = 'primary',
  icon,
  children,
  ...props
}: ButtonProps) {
  return (
    <button
      {...props}
      className={`
        rounded-lg px-4 py-2 font-medium transition-colors
        focus-visible:outline-2 focus-visible:outline-offset-2
        ${variantStyles[variant]}
      `}
    >
      {icon && <span aria-hidden="true">{icon}</span>}
      {children}
    </button>
  );
}

const variantStyles = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
  danger: 'bg-red-600 text-white hover:bg-red-700',
};

Automated accessibility testing

ESLint plugin

json// .eslintrc.json
{
  "extends": ["plugin:jsx-a11y/recommended"],
  "rules": {
    "jsx-a11y/click-events-have-key-events": "error",
    "jsx-a11y/no-static-element-interactions": "error",
    "jsx-a11y/anchor-is-valid": "error",
    "jsx-a11y/alt-text": "error"
  }
}

Playwright accessibility testing

typescript// tests/e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import { injectAxe, checkA11y } from 'axe-playwright';

test.describe('Accessibility', () => {
  test.beforeEach(async ({ page }) => {
    await injectAxe(page);
  });

  test('home page has no accessibility violations', async ({ page }) => {
    await page.goto('/');

    // Check for WCAG 2.1 Level A and AA violations
    const results = await checkA11y(page, {
      detailedReport: true,
      detailedReportOptions: { html: true },
    });

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

  test('modal is keyboard accessible', async ({ page }) => {
    await page.goto('/');

    // Open modal via keyboard
    await page.keyboard.press('Tab');
    await page.keyboard.press('Enter');

    // Verify modal is focused
    const modal = page.locator('[role="dialog"]');
    await expect(modal).toBeFocused();

    // Verify focus trap works
    await page.keyboard.press('Tab');
    await expect(modal.locator('button:first-child')).toBeFocused();
  });
});

CI integration

yaml# .github/workflows/accessibility.yml
name: Accessibility Tests

on:
  pull_request:
    paths:
      - 'src/**'

jobs:
  accessibility:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Run accessibility tests
        run: npx playwright test tests/e2e/accessibility

      - name: Upload accessibility report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: accessibility-report
          path: playwright-report/

Common accessibility failures

1. Missing alt text

html<!-- Bad -->
<img src="product.jpg" />

<!-- Good -->
<img src="product.jpg" alt="Product photo showing blue t-shirt" />
<!-- Good: Decorative image -->
<img src="background.jpg" alt="" role="presentation" />

2. Unlabeled form inputs

html<!-- Bad -->
<input type="email" placeholder="Enter your email" />

<!-- Good -->
<label for="email">Email</label>
<input id="email" type="email" />

<!-- Also good -->
<label>
  Email
  <input type="email" />
</label>

3. Keyboard traps

Elements that trap keyboard focus without escape mechanisms:

typescript// Bad: Focus cannot escape
function Modal({ children }: ModalProps) {
  return <div className="fixed inset-0">{children}</div>;
}

// Good: Focus can escape via ESC key
function Modal({ onClose, children }: ModalProps) {
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
    }
  };

  return (
    <div onKeyDown={handleKeyDown} className="fixed inset-0">
      {children}
    </div>
  );
}

4. Missing heading hierarchy

html<!-- Bad: Jumping heading levels -->
<h1>Main title</h1>
<h3>Subsection</h3>
<h5>Detail</h5>

<!-- Good: Proper hierarchy -->
<h1>Main title</h1>
<h2>Section</h2>
<h3>Subsection</h3>

The accessibility-first workflow

Integrate accessibility throughout your development process:

  1. Design phase: Use accessible design patterns. Check color contrast early.
  2. Development phase: Use semantic HTML. Add ARIA attributes when needed. Ensure keyboard navigation works.
  3. Code review phase: Review for accessibility issues. Use ESLint rules to catch common problems.
  4. Testing phase: Run automated accessibility tests. Test with screen readers and keyboard navigation.
  5. Deployment phase: Monitor accessibility metrics. Track and fix accessibility bugs.

Conclusion

Web accessibility is not a compliance exercise—it's a fundamental quality attribute that makes your product usable by everyone. By integrating accessibility into your engineering workflow from day one, you build more inclusive products while avoiding the technical debt of retrofitting.

Start with semantic HTML as your foundation. Ensure keyboard navigation works for all interactive elements. Provide proper labels and ARIA attributes for screen readers. Test color contrast ratios. Automate accessibility testing in CI/CD.

The investment pays off in a larger addressable market, reduced legal risk, and better user experience for everyone—not just users with disabilities.


Your web application isn't meeting accessibility standards or you're unsure where to start? Talk to Imperialis web development specialists to implement accessibility as a core engineering practice and build truly inclusive products.

Sources

Related reading