Web Development 3 min read 690 views

Building Accessible Web Applications: A Developer's Complete Guide to WCAG 2.2

Create inclusive web experiences that work for everyone. This comprehensive guide covers WCAG 2.2 guidelines, testing tools, and practical implementation patterns.

E
Accessibility and inclusive design

Building accessible web applications isn't just about compliance—it's about creating experiences that work for everyone. With WCAG 2.2 now the standard, this guide covers practical implementation strategies for developers.

Understanding WCAG 2.2

WCAG 2.2 is organized around four principles (POUR):

  1. Perceivable: Information must be presentable in ways users can perceive
  2. Operable: Interface components must be operable by all users
  3. Understandable: Information and operation must be understandable
  4. Robust: Content must be robust enough for assistive technologies

Essential Accessibility Patterns

Semantic HTML

<!-- Bad: div soup -->
<div class="header">
    <div class="nav">
        <div class="nav-item">Home</div>
    </div>
</div>

<!-- Good: semantic elements -->
<header>
    <nav aria-label="Main navigation">
        <ul>
            <li><a href="/">Home</a></li>
        </ul>
    </nav>
</header>

Accessible Forms

<form>
    <div class="form-group">
        <label for="email">Email address</label>
        <input
            type="email"
            id="email"
            name="email"
            aria-describedby="email-help email-error"
            aria-invalid="true"
            required
        >
        <p id="email-help" class="help-text">
            We'll never share your email.
        </p>
        <p id="email-error" class="error" role="alert">
            Please enter a valid email address.
        </p>
    </div>

    <button type="submit">Subscribe</button>
</form>

Keyboard Navigation

// Custom dropdown with keyboard support
function Dropdown({ options, onChange }) {
    const [isOpen, setIsOpen] = useState(false);
    const [focusIndex, setFocusIndex] = useState(0);

    const handleKeyDown = (e) => {
        switch (e.key) {
            case 'ArrowDown':
                e.preventDefault();
                setFocusIndex(i => Math.min(i + 1, options.length - 1));
                break;
            case 'ArrowUp':
                e.preventDefault();
                setFocusIndex(i => Math.max(i - 1, 0));
                break;
            case 'Enter':
            case ' ':
                e.preventDefault();
                onChange(options[focusIndex]);
                setIsOpen(false);
                break;
            case 'Escape':
                setIsOpen(false);
                break;
        }
    };

    return (
        <div
            role="listbox"
            tabIndex={0}
            onKeyDown={handleKeyDown}
            aria-activedescendant={\`option-\${focusIndex}\`}
        >
            {options.map((option, i) => (
                <div
                    key={option.value}
                    id={\`option-\${i}\`}
                    role="option"
                    aria-selected={i === focusIndex}
                >
                    {option.label}
                </div>
            ))}
        </div>
    );
}

Color and Contrast

WCAG 2.2 requires:

  • Normal text: 4.5:1 contrast ratio
  • Large text (18pt+): 3:1 contrast ratio
  • UI components: 3:1 against adjacent colors
/* CSS custom properties for accessible colors */
:root {
    --text-primary: #1a1a1a;     /* 15:1 on white */
    --text-secondary: #595959;   /* 7:1 on white */
    --background: #ffffff;
    --accent: #0066cc;           /* 5.9:1 on white */
    --error: #d32f2f;            /* 4.8:1 on white */
}

ARIA Patterns

Live Regions

<!-- Announce dynamic content changes -->
<div aria-live="polite" aria-atomic="true">
    {{ statusMessage }}
</div>

<!-- For urgent alerts -->
<div role="alert">
    Your session will expire in 2 minutes.
</div>

Modal Dialogs

<div
    role="dialog"
    aria-modal="true"
    aria-labelledby="dialog-title"
    aria-describedby="dialog-description"
>
    <h2 id="dialog-title">Confirm deletion</h2>
    <p id="dialog-description">
        This action cannot be undone.
    </p>
    <button onClick={onConfirm}>Delete</button>
    <button onClick={onCancel}>Cancel</button>
</div>

Testing Tools

  • axe DevTools: Browser extension for automated testing
  • WAVE: Web accessibility evaluation tool
  • Lighthouse: Built into Chrome DevTools
  • Screen readers: NVDA (Windows), VoiceOver (macOS/iOS)
// Automated testing with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('form is accessible', async () => {
    const { container } = render(<ContactForm />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
});

Accessibility is a journey, not a destination. Start with the basics, test regularly, and continuously improve.

Share this article:
ES

Written by Edrees Salih

Full-stack software engineer with 9 years of experience. Passionate about building scalable solutions and sharing knowledge with the developer community.

View Profile

Comments (0)

Leave a Comment

Your email will not be published.

No comments yet. Be the first to share your thoughts!