Why Accessibility Matters: Building Inclusive User Interfaces

By Daian Scuarissi
accessibilityreactweb-developmentinclusive-designa11ywcag

Web accessibility isn't just a nice-to-have feature—it's a fundamental requirement for building truly inclusive digital experiences. With over 1.3 billion people worldwide living with disabilities, ensuring our applications are accessible isn't just the right thing to do; it's essential for reaching the broadest possible audience and creating software that works for everyone.

What is Web Accessibility?

Web accessibility (often abbreviated as a11y) refers to the practice of designing and developing websites, applications, and digital tools that can be used by people with disabilities. This includes individuals with:

The goal is to ensure that digital content and functionality are perceivable, operable, understandable, and robust for all users, regardless of their abilities or the assistive technologies they use.

The Business Case for Accessibility

Beyond the moral imperative, there are compelling business reasons to prioritize accessibility:

Legal Requirements

Market Reach and Revenue

Technical Benefits

Accessibility in Practice: A Real-World Example

Let's examine a React component that demonstrates accessibility best practices. This Experience Section component showcases several key accessibility principles:

'use client';
import { ResumeButton } from '../../ResumeButton';
import { EXPERIENCE_ITEMS } from '../Data/ExperienceItems';
import { Reveal } from '@/components/Utils/Reveal';
import { Briefcase, Link, Zap, ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
 
const ExperienceSection = () => {
  const array = EXPERIENCE_ITEMS;
  const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set());
 
  const toggleExpanded = (index: number) => {
    const newExpanded = new Set(expandedItems);
    if (newExpanded.has(index)) {
      newExpanded.delete(index);
    } else {
      newExpanded.add(index);
    }
    setExpandedItems(newExpanded);
  };
 
  return (
    <section
      aria-labelledby='experience-heading'
      id='experience'
      className='scroll-mt-16'
    >
      <div className='container mx-auto py-16 md:py-24'>
        <header className='text-center mb-16'>
          <h2
            id='experience-heading'
            className='text-4xl md:text-5xl font-bold mb-4'
          >
            Experience
          </h2>
          <div className='w-16 h-1 mx-auto bg-brand rounded-full' />
        </header>
 
        <section className='overflow-hidden md:px-4 md:py-12'>
          <div className='container max-w-5xl mx-auto p-0 relative timelineVertical'>
            {array.map(
              (
                {
                  company,
                  companyUrl,
                  from,
                  to,
                  role,
                  description,
                  actual,
                  client,
                  keywords,
                },
                i
              ) => (
                <div
                  className='timeline-item md:even:flex-row-reverse mb-8'
                  key={i}
                >
                  <Reveal index={i}>
                    <div
                      className={`flex ${i % 2 ? 'md:flex-row-reverse' : ''}`}
                    >
                      <div className='image flex items-center justify-center md:order-1 shrink-0 w-16 h-16 shadow-lg rounded-full bg-brand'>
                        <Briefcase className='inline-block text-white w-7 h-7' />
                      </div>
                      <div className='relative content bg-slate-200 p-4 grow md:grow-0 md:w-2/5 ribbon rounded-xl'>
                        <div className='flex justify-between flex-col gap-0.5 mb-2 md:flex-row'>
                          <a
                            href={companyUrl}
                            target='_blank'
                            rel='noreferrer'
                            aria-label={`company URL: ${company}`}
                          >
                            <h3 className='text-blue-600 font-semibold cursor-pointer flex items-center gap-2 text-xl md:mb-0'>
                              {company}
                              <Link />
                            </h3>
                          </a>
                          <div className='flex justify-between items-center mb-1 md:mb-0'>
                            <span className='text-slate-500 text-sm md:text-base'>
                              {from} - {actual ? 'Present' : to}
                            </span>
                          </div>
                        </div>
 
                        <div className='flex justify-between items-center'>
                          <span className='dark:text-black'>
                            Role: <span className='font-bold'>{role}</span>{' '}
                            {client && <span>| Client: {client}</span>}
                          </span>
                          <button
                            onClick={() => toggleExpanded(i)}
                            className='ml-2 p-1 rounded-md hover:bg-slate-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1'
                            aria-label={
                              expandedItems.has(i)
                                ? 'Hide details'
                                : 'Show details'
                            }
                          >
                            {expandedItems.has(i) ? (
                              <ChevronUp className='w-5 h-5 text-slate-600' />
                            ) : (
                              <ChevronDown className='w-5 h-5 text-slate-600' />
                            )}
                          </button>
                        </div>
 
                        <div
                          className={`overflow-hidden transition-all duration-300 ease-in-out ${
                            expandedItems.has(i)
                              ? 'max-h-full opacity-100 mt-3'
                              : 'max-h-0 opacity-0'
                          }`}
                        >
                          {description.map((item, idx) => (
                            <p key={idx} className='dark:text-black mt-2'>
                              - {item}
                            </p>
                          ))}
                          {keywords && (
                            <div className='dark: text-black mt-2'>
                              Keywords: <span>{keywords}</span>
                            </div>
                          )}
                        </div>
                      </div>
                    </div>
                  </Reveal>
                </div>
              )
            )}
          </div>
          <div className='md:flex items-center justify-center p-4 pl-0 pt-0 md:pl-4 mt-[-3rem]'>
            <div className='flex items-center justify-center w-16 h-16 shadow-lg rounded-full bg-brand'>
              <Zap className='inline-block text-white w-7 h-7' />
            </div>
          </div>
        </section>
        <ResumeButton />
      </div>
    </section>
  );
};
 
export default ExperienceSection;

Accessibility Features Deep-Dive

Let's break down the accessibility features implemented in this component:

1. Semantic HTML Structure

What it does: Uses appropriate HTML elements that convey meaning to assistive technologies.

<section aria-labelledby='experience-heading' id='experience'>
  <header className='text-center mb-16'>
    <h2 id='experience-heading'>Experience</h2>
  </header>
</section>

Why it matters:

2. ARIA Labels and Relationships

What it does: Provides additional context for assistive technologies through ARIA attributes.

<section aria-labelledby='experience-heading' id='experience'>
<a
  href={companyUrl}
  target='_blank'
  rel='noreferrer'
  aria-label={`company URL: ${company}`}
>

Why it matters:

3. Keyboard Navigation Support

What it does: Ensures all interactive elements are keyboard accessible.

<button
  onClick={() => toggleExpanded(i)}
  className='ml-2 p-1 rounded-md hover:bg-slate-300 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1'
  aria-label={expandedItems.has(i) ? 'Hide details' : 'Show details'}
>

Key features:

4. Dynamic Content Accessibility

What it does: Provides context for content that changes dynamically.

<button aria-label={expandedItems.has(i) ? 'Hide details' : 'Show details'}>
  {expandedItems.has(i) ? (
    <ChevronUp className='w-5 h-5 text-slate-600' />
  ) : (
    <ChevronDown className='w-5 h-5 text-slate-600' />
  )}
</button>

Why it's important:

5. Visual Design for Accessibility

What it does: Uses visual cues that support accessibility principles.

<div className='w-16 h-1 mx-auto bg-brand rounded-full' />
className =
  'text-blue-600 font-semibold cursor-pointer flex items-center gap-2';

Accessibility benefits:

Best Practices Extracted from the Example

Based on our component analysis, here are actionable accessibility guidelines you can implement immediately:

1. Always Use Semantic HTML

// ✅ Good: Semantic structure
<section aria-labelledby="main-heading">
  <header>
    <h2 id="main-heading">Section Title</h2>
  </header>
  <article>Content here</article>
</section>
 
// ❌ Bad: Generic divs everywhere
<div>
  <div>
    <div>Section Title</div>
  </div>
  <div>Content here</div>
</div>

2. Provide Meaningful ARIA Labels

// ✅ Good: Descriptive labels
<button
  aria-label="Close navigation menu"
  onClick={closeMenu}
>
  <X />
</button>
 
// ❌ Bad: No context for screen readers
<button onClick={closeMenu}>
  <X />
</button>

3. Implement Proper Focus Management

// ✅ Good: Visible focus indicators
const buttonStyles = `
  focus:outline-none 
  focus:ring-2 
  focus:ring-blue-500 
  focus:ring-offset-2
  transition-all 
  duration-200
`;
 
// ✅ Good: Focus trapping in modals
useEffect(() => {
  if (isModalOpen) {
    const firstFocusable = modalRef.current?.querySelector(
      'button, [href], input, select, textarea'
    );
    firstFocusable?.focus();
  }
}, [isModalOpen]);

4. Create Dynamic Content Responsively

// ✅ Good: State changes are communicated
<button
  aria-expanded={isExpanded}
  aria-controls="content-panel"
  aria-label={isExpanded ? 'Collapse section' : 'Expand section'}
>
  {isExpanded ? <ChevronUp /> : <ChevronDown />}
</button>
 
<div
  id="content-panel"
  role="region"
  aria-hidden={!isExpanded}
>
  Content here
</div>

5. Test with Real Users and Tools

// ✅ Good: Include accessibility testing
describe('ExperienceSection Accessibility', () => {
  it('should have proper heading structure', () => {
    render(<ExperienceSection />);
    expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
  });
 
  it('should support keyboard navigation', () => {
    render(<ExperienceSection />);
    const expandButton = screen.getByRole('button', { name: /show details/i });
    fireEvent.keyDown(expandButton, { key: 'Enter' });
    expect(screen.getByText(/hide details/i)).toBeInTheDocument();
  });
});

Essential Tools for Accessibility Testing

Automated Testing Tools

  1. axe-core DevTools Extension

    • Browser extension for Chrome, Firefox, Edge
    • Identifies common accessibility issues
    • Provides specific remediation guidance
  2. Lighthouse Accessibility Audit

    # Run programmatically
    npm install -g lighthouse
    lighthouse https://yoursite.com --only-categories=accessibility
  3. ESLint Accessibility Plugins

    npm install eslint-plugin-jsx-a11y
    // .eslintrc.json
    {
      "extends": ["plugin:jsx-a11y/recommended"],
      "plugins": ["jsx-a11y"]
    }

Manual Testing Approaches

  1. Keyboard Navigation Testing

    • Tab through your entire interface
    • Ensure all interactive elements are reachable
    • Test with screen readers (NVDA, JAWS, VoiceOver)
  2. Screen Reader Testing

    // Test with various screen readers
    // - NVDA (Windows, free)
    // - JAWS (Windows, paid)
    // - VoiceOver (macOS/iOS, built-in)
    // - TalkBack (Android, built-in)
  3. Color and Contrast Validation

    • Use tools like WebAIM's Contrast Checker
    • Test with color blindness simulators
    • Ensure information isn't conveyed by color alone

Advanced Testing with Playwright

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
 
test('should not have accessibility violations', async ({ page }) => {
  await page.goto('/experience');
 
  const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
 
  expect(accessibilityScanResults.violations).toEqual([]);
});
 
test('keyboard navigation works correctly', async ({ page }) => {
  await page.goto('/experience');
 
  // Test tab navigation
  await page.keyboard.press('Tab');
  await expect(
    page.locator('button[aria-label*="Show details"]').first()
  ).toBeFocused();
 
  // Test activation with keyboard
  await page.keyboard.press('Enter');
  await expect(
    page.locator('button[aria-label*="Hide details"]').first()
  ).toBeFocused();
});

The WCAG Guidelines: Your Accessibility Roadmap

The Web Content Accessibility Guidelines (WCAG) 2.1 provide the international standard for web accessibility. They're organized around four principles:

1. Perceivable

Information must be presentable in ways users can perceive.

// ✅ Alt text for images
<img src='/company-logo.png' alt='TechCorp company logo' />;
 
// ✅ Sufficient color contrast
const styles = {
  color: '#1f2937', // 16.94:1 contrast ratio on white
  backgroundColor: '#ffffff',
};

2. Operable

Interface components must be operable.

// ✅ Keyboard accessible
<div
  role='button'
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick();
    }
  }}
  onClick={handleClick}
>
  Custom Button
</div>

3. Understandable

Information and operation must be understandable.

// ✅ Clear error messages
<input type='email' aria-describedby='email-error' aria-invalid={hasError} />;
{
  hasError && (
    <div id='email-error' role='alert'>
      Please enter a valid email address (e.g., user@example.com)
    </div>
  );
}

4. Robust

Content must be robust enough for various assistive technologies.

// ✅ Valid HTML structure
<nav role='navigation' aria-label='Main navigation'>
  <ul>
    <li>
      <a href='/home'>Home</a>
    </li>
    <li>
      <a href='/about'>About</a>
    </li>
  </ul>
</nav>

Advanced Accessibility Patterns

Skip Links for Keyboard Users

const SkipLink = () => (
  <a
    href='#main-content'
    className='sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-blue-600 text-white p-2 rounded'
  >
    Skip to main content
  </a>
);

Live Regions for Dynamic Updates

const StatusMessage = ({ message, type }) => (
  <div
    role='status'
    aria-live={type === 'error' ? 'assertive' : 'polite'}
    aria-atomic='true'
    className='sr-only'
  >
    {message}
  </div>
);

Focus Trapping in Modals

const Modal = ({ isOpen, onClose, children }) => {
  const modalRef = useRef(null);
 
  useEffect(() => {
    if (isOpen) {
      const focusableElements = modalRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
 
      const firstElement = focusableElements[0];
      const lastElement = focusableElements[focusableElements.length - 1];
 
      const trapFocus = (e) => {
        if (e.key === 'Tab') {
          if (e.shiftKey && document.activeElement === firstElement) {
            e.preventDefault();
            lastElement.focus();
          } else if (!e.shiftKey && document.activeElement === lastElement) {
            e.preventDefault();
            firstElement.focus();
          }
        }
 
        if (e.key === 'Escape') {
          onClose();
        }
      };
 
      document.addEventListener('keydown', trapFocus);
      firstElement?.focus();
 
      return () => document.removeEventListener('keydown', trapFocus);
    }
  }, [isOpen, onClose]);
 
  if (!isOpen) return null;
 
  return (
    <div
      role='dialog'
      aria-modal='true'
      aria-labelledby='modal-title'
      ref={modalRef}
      className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center'
    >
      <div className='bg-white p-6 rounded-lg max-w-md w-full'>{children}</div>
    </div>
  );
};

Building an Accessibility-First Culture

Development Process Integration

  1. Design Phase

    • Include accessibility requirements in user stories
    • Design with sufficient color contrast from the start
    • Plan keyboard navigation flows
  2. Development Phase

    • Use semantic HTML as the foundation
    • Implement accessibility features alongside functionality
    • Write accessibility tests with your unit tests
  3. Testing Phase

    • Automated accessibility testing in CI/CD
    • Manual testing with keyboard and screen readers
    • User testing with people who use assistive technologies
  4. Review Process

    • Include accessibility checks in code reviews
    • Test accessibility in staging environments
    • Monitor accessibility in production

Team Education and Resources

// Document accessibility patterns in your component library
/**
 * Button Component
 *
 * @accessibility
 * - Supports keyboard navigation (Enter/Space)
 * - Maintains focus indicators
 * - Provides proper contrast ratios
 * - Includes appropriate ARIA attributes when needed
 *
 * @example
 * <Button
 *   variant="primary"
 *   aria-label="Save document" // Use when button text isn't descriptive
 * >
 *   Save
 * </Button>
 */
const Button = ({ children, onClick, 'aria-label': ariaLabel, ...props }) => {
  return (
    <button
      onClick={onClick}
      aria-label={ariaLabel}
      className='px-4 py-2 bg-blue-600 text-white rounded focus:ring-2 focus:ring-blue-500 focus:ring-offset-2'
      {...props}
    >
      {children}
    </button>
  );
};

Conclusion: Accessibility is Not Optional

Web accessibility is not a feature to be added later—it's a fundamental aspect of quality web development. As we've seen through our React component example, building accessible interfaces requires intentional design decisions, but these decisions often result in better code architecture and improved user experiences for everyone.

Key Takeaways

  1. Start with Semantics: Use HTML elements for their intended purpose
  2. Enhance with ARIA: Provide additional context where HTML falls short
  3. Test Early and Often: Integrate accessibility testing into your development workflow
  4. Learn from Users: Include people with disabilities in your testing process
  5. Make it Systematic: Build accessibility into your design system and development standards

The Path Forward

Accessibility is an ongoing commitment, not a one-time implementation. As web technologies evolve, so do the tools and techniques for building inclusive experiences. By making accessibility a core part of your development practice, you're not just compliance with legal requirements—you're creating digital experiences that truly serve all users.

Remember: Good accessibility is invisible. When done well, users don't notice the accessibility features—they just experience a well-designed, intuitive interface that works exactly as they expect it to.

The investment in accessibility pays dividends not just in legal compliance and market reach, but in the quality and robustness of your codebase. Every developer who learns to build accessible interfaces becomes a better developer overall, creating software that is more semantic, more testable, and more maintainable.

Let's build a web that works for everyone.


Want to dive deeper into accessibility? Check out the WCAG 2.1 Guidelines, explore the WebAIM resources, and start testing your applications with screen readers today.