design

Web Accessibility: WCAG 2.2 Standards praktisch umsetzen

Kompletter WCAG-Guide: Barrierefreie Websites entwickeln, ARIA richtig nutzen, Keyboard-Navigation und Screen-Reader Unterstützung implementieren.

Onur Cirakoglu
10 min read
#Accessibility#WCAG#ARIA#Screen Reader#Inclusive Design
Barrierefreies Web Design und Accessibility Icons

15% der Weltbevölkerung leben mit einer Behinderung. Barrierefreie Websites sind nicht nur ethisch richtig - sie sind oft gesetzlich vorgeschrieben und erreichen 30% mehr Nutzer. Dieser Guide zeigt, wie Sie WCAG 2.2 Level AA erfüllen.

WCAG Prinzipien verstehen

Die vier POUR-Prinzipien:

1. Perceivable (Wahrnehmbar)

Informationen müssen für alle wahrnehmbar sein.

// ❌ Bild ohne Alt-Text
<img src="/product.jpg" />
 
// ✅ Mit aussagekräftigem Alt-Text
<Image src="/product.jpg" alt="Rotes Kleid mit Blumenmuster, Größe M" />

2. Operable (Bedienbar)

Alle Funktionen per Tastatur nutzbar.

// ❌ Nur mit Maus bedienbar
<div onClick={handleClick}>Click me</div>
 
// ✅ Keyboard accessible
<button onClick={handleClick} onKeyDown={(e) => e.key === 'Enter' && handleClick()}>
  Click me
</button>

3. Understandable (Verständlich)

Content und Bedienung müssen verständlich sein.

// ✅ Klare Fehlermeldungen
<Input
  error="E-Mail-Adresse ist erforderlich. Bitte geben Sie eine gültige E-Mail ein."
  aria-describedby="email-error"
/>

4. Robust (Robust)

Funktioniert mit verschiedenen Technologien.

// ✅ Semantisches HTML
<nav aria-label="Hauptnavigation">
  <ul role="list">
    <li><a href="/">Home</a></li>
  </ul>
</nav>

Semantisches HTML

// ❌ Div-Suppe
<div class="header">
  <div class="nav">
    <div class="link" onclick="navigate()">Home</div>
  </div>
</div>
 
// ✅ Semantisch korrekt
<header>
  <nav aria-label="Hauptnavigation">
    <a href="/">Home</a>
  </nav>
</header>

ARIA Richtig nutzen

ARIA-Labels

// Icon-Button ohne Text
<button aria-label="Warenkorb öffnen">
  <ShoppingCart />
</button>
 
// Search Input
<input
  type="search"
  placeholder="Suchen..."
  aria-label="Website durchsuchen"
/>

ARIA-Roles

<div role="alert" aria-live="polite">
  Ihre Änderungen wurden gespeichert
</div>
 
<div role="navigation" aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
  </ol>
</div>

ARIA-States

<button
  aria-expanded={isOpen}
  aria-controls="menu"
  onClick={() => setIsOpen(!isOpen)}
>
  Menu
</button>
 
<div id="menu" aria-hidden={!isOpen}>
  Menu content
</div>

Keyboard Navigation

'use client'
 
export function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null)
 
  useEffect(() => {
    if (!isOpen) return
 
    // Trap focus inside modal
    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
 
    const handleTab = (e: KeyboardEvent) => {
      if (e.key !== 'Tab') return
 
      if (e.shiftKey) {
        if (document.activeElement === firstElement) {
          lastElement?.focus()
          e.preventDefault()
        }
      } else {
        if (document.activeElement === lastElement) {
          firstElement?.focus()
          e.preventDefault()
        }
      }
    }
 
    // Close on Escape
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose()
    }
 
    document.addEventListener('keydown', handleTab)
    document.addEventListener('keydown', handleEscape)
    firstElement?.focus()
 
    return () => {
      document.removeEventListener('keydown', handleTab)
      document.removeEventListener('keydown', handleEscape)
    }
  }, [isOpen, onClose])
 
  if (!isOpen) return null
 
  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      {children}
    </div>
  )
}

Focus Management

export function Dropdown() {
  const [isOpen, setIsOpen] = useState(false)
  const buttonRef = useRef<HTMLButtonElement>(null)
  const menuRef = useRef<HTMLDivElement>(null)
 
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'ArrowDown') {
      setIsOpen(true)
      // Focus first menu item
      setTimeout(() => {
        const firstItem = menuRef.current?.querySelector('[role="menuitem"]') as HTMLElement
        firstItem?.focus()
      }, 0)
    }
  }
 
  return (
    <div>
      <button
        ref={buttonRef}
        aria-haspopup="true"
        aria-expanded={isOpen}
        onKeyDown={handleKeyDown}
        onClick={() => setIsOpen(!isOpen)}
      >
        Menu
      </button>
 
      {isOpen && (
        <div ref={menuRef} role="menu">
          <button role="menuitem">Option 1</button>
          <button role="menuitem">Option 2</button>
        </div>
      )}
    </div>
  )
}

Screen Reader Support

// Live Regions für dynamischen Content
export function StatusMessage() {
  const [status, setStatus] = useState('')
 
  return (
    <div
      role="status"
      aria-live="polite"
      aria-atomic="true"
      className="sr-only"
    >
      {status}
    </div>
  )
}
 
// SR-only Utility Class
// Visuell versteckt, für Screen Reader sichtbar
<span className="sr-only">
  Ergebnis wird geladen
</span>
<Spinner aria-hidden="true" />
/* globals.css */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Color Contrast

// ❌ Schlechter Kontrast (2.5:1)
<p className="text-gray-400 bg-gray-200">Text</p>
 
// ✅ Guter Kontrast (4.5:1 für Text, 3:1 für UI)
<p className="text-gray-900 bg-white">Text</p>
 
// Für Large Text (18pt+): Min 3:1
<h1 className="text-4xl text-gray-700">Heading</h1>

Tools:

Forms Accessibility

export function AccessibleForm() {
  return (
    <form>
      <div className="space-y-1">
        <label htmlFor="email" className="block text-sm font-medium">
          E-Mail *
        </label>
        <input
          id="email"
          type="email"
          required
          aria-required="true"
          aria-describedby="email-desc email-error"
        />
        <p id="email-desc" className="text-sm text-gray-600">
          Wir senden keine Spam-Mails
        </p>
        <p id="email-error" role="alert" className="text-sm text-red-600">
          Bitte geben Sie eine gültige E-Mail ein
        </p>
      </div>
 
      <fieldset>
        <legend>Newsletter abonnieren</legend>
        <div>
          <input
            type="radio"
            id="weekly"
            name="frequency"
            value="weekly"
          />
          <label htmlFor="weekly">Wöchentlich</label>
        </div>
        <div>
          <input
            type="radio"
            id="monthly"
            name="frequency"
            value="monthly"
          />
          <label htmlFor="monthly">Monatlich</label>
        </div>
      </fieldset>
    </form>
  )
}
// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <a
          href="#main-content"
          className="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 z-50 bg-white px-4 py-2"
        >
          Zum Hauptinhalt springen
        </a>
 
        <Header />
 
        <main id="main-content" tabIndex={-1}>
          {children}
        </main>
 
        <Footer />
      </body>
    </html>
  )
}

Testing Tools

Automated Testing:

# axe-core für Jest
pnpm add -D @axe-core/react jest-axe
 
# Lighthouse CI
pnpm add -D @lhci/cli
// __tests__/accessibility.test.tsx
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Button } from '@/components/ui/button'
 
expect.extend(toHaveNoViolations)
 
it('should have no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>)
  const results = await axe(container)
  expect(results).toHaveNoViolations()
})

Manual Testing:

  • NVDA (Windows Screen Reader)
  • JAWS (Windows Screen Reader)
  • VoiceOver (Mac/iOS)
  • TalkBack (Android)
  • Keyboard-only Navigation

WCAG 2.2 Checklist

Level A (Minimum):

  • [ ] Alt-Text für alle Images
  • [ ] Keyboard-Navigation möglich
  • [ ] Keine Timeouts ohne Warnung
  • [ ] Kein blinkender Content

Level AA (Empfohlen):

  • [ ] Kontrast 4.5:1 (Text) / 3:1 (UI)
  • [ ] Resizable Text bis 200%
  • [ ] Heading-Struktur logisch
  • [ ] Focus-Indikatoren sichtbar
  • [ ] Error Identification & Suggestions

Level AAA (Enhanced):

  • [ ] Kontrast 7:1
  • [ ] Sign Language für Audio
  • [ ] Keine zeitbasierten Medien

Zusammenfassung

Barrierefreie Websites erreichen:

30% mehr NutzerBesseres SEO-RankingGesetzliche ComplianceBessere UX für alle

Beginnen Sie mit semantischem HTML, ARIA und Keyboard-Navigation.

Accessibility Audit?

Als UX-Spezialisten bieten wir:

  • WCAG 2.2 Audits
  • 🔧 Accessibility Fixes
  • 📚 Team-Schulungen
  • Fortlaufende Compliance

Audit anfragen

Aktualisiert: März 2024

#Accessibility#WCAG#ARIA#Screen Reader#Inclusive Design