Web Accessibility: WCAG 2.1 AA Compliance Guide

Build WCAG 2.1 AA compliant web apps with this practical guide. Covers semantic HTML, ARIA, keyboard navigation, color contrast, screen readers, and automated testing tools.

E
ECOSIRE Research and Development Team
|19 Mart 20269 dk okuma2.0k Kelime|

Compliance & Regulation serimizin bir parçası

Tam kılavuzu okuyun

Web Erişilebilirliği: WCAG 2.1 AA Uyumluluk Kılavuzu

Erişilebilirlik, lansmandan sonra eklediğiniz bir özellik değildir; performans veya güvenlikle aynı olan temel bir kalite özelliğidir. WCAG 2.1 AA uyumluluğu artık AB'de (Avrupa Erişilebilirlik Yasası, Haziran 2025'te yürürlüğe girmiştir), ABD'de (ADA Başlık III içtihat hukuku) ve diğer birçok yargı bölgesinde yasal olarak zorunludur. Uyumluluğun ötesinde, erişilebilir arayüzler daha iyi dönüşüm sağlıyor, aramalarda daha üst sıralarda yer alıyor ve dünya çapında tahminen 1,3 milyar engelli insana hizmet veriyor.

Bu kılavuz bir kontrol listesi değil, pratik bir uygulama kılavuzudur. Dört WCAG ilkesini, en etkili teknikleri, sistematik olarak nasıl test yapacağınızı ve erişilebilirliği React/Next.js geliştirme iş akışınıza nasıl entegre edeceğinizi, böylece sabit kalmasını öğreneceksiniz.

Önemli Çıkarımlar

  • WCAG 2.1 AA, dört POUR ilkesinin tümünü gerektirir: Algılanabilir, Çalıştırılabilir, Anlaşılabilir, Sağlam
  • Anlamsal HTML ile başlayın — herhangi bir ARIA eklenmeden önce %70 ücretsiz erişilebilirlik sağlar
  • Minimum renk kontrast oranı: normal metin için 4,5:1, büyük metin için 3:1 (18 punto/14 punto kalın)
  • Her etkileşimli öğe, görünür bir odak göstergesiyle klavyeyle odaklanabilir olmalıdır
  • Ekran okuyucular erişilebilirlik ağacına göre duyuru yapar — NVDA (Windows) ve VoiceOver (Mac) ile test edin
  • ARIA son çaredir; davranışı değil, yalnızca yardımcı teknolojilerin DOM'u nasıl yorumladığını değiştirir
  • CI hattınızda axe-core ile otomatikleştirin; manuel test, otomasyonun kaçırdığı şeyleri yakalar
  • Erişilebilirlik bildiriminizi belgeleyin ve kullanıcıların sorunları bildirmesi için bir geri bildirim mekanizması sağlayın

Dört POUR Prensibi

WCAG 2.1 dört ilke etrafında düzenlenmiştir. Her başarı kriteri bunlardan birine aittir.

Algılanabilir: Bilgi, kullanıcıların algılayabileceği şekilde sunulabilir olmalıdır. Bu, görseller için metin alternatiflerini, video altyazılarını, yeterli renk kontrastını ve anlamı iletmek için yalnızca renge dayanmayan içeriği kapsar.

Çalıştırılabilir: Tüm işlevler klavye aracılığıyla çalıştırılabilir olmalı, etkileşim için yeterli süreye sahip olmalı, ele geçirmeyi tetikleyen içerik bulunmamalı ve gezinilebilir yapı (atlama bağlantıları, sayfa başlıkları, odak sırası) olmalıdır.

Anlaşılabilir: İçerik okunabilir ve tahmin edilebilir olmalıdır. Dil tanımlanmalı, hata mesajları açıklayıcı olmalı ve formlarda açık etiketler ve doğrulama geri bildirimi bulunmalıdır.

Sağlam: İçerik, mevcut ve gelecekteki yardımcı teknolojiler tarafından yorumlanabilir olmalıdır. Bu, geçerli HTML, uygun ARIA kullanımı ve odaklanma gerektirmeden duyurulan durum mesajları anlamına gelir.


Önce Semantik HTML

Anlamsal HTML, en yüksek kaldıraca sahip erişilebilirlik yatırımıdır. Yerel HTML öğeleri yerleşik erişilebilirlik rolleri, durumları ve klavye davranışıyla birlikte gelir; ARIA gerekmez.

// BAD: Generic divs with no semantics
<div class="button" onclick="submit()">Submit</div>
<div class="nav">
  <div class="link" onclick="navigate('/home')">Home</div>
</div>

// GOOD: Native semantics, free keyboard and screen reader support
<button type="submit" onClick={submit}>Submit</button>
<nav aria-label="Main navigation">
  <a href="/home">Home</a>
</nav>

Önemli nokta bölgeleri, ekran okuyucu kullanıcılarının bölümler arasında geçiş yaparak hızlı bir şekilde gezinmesine yardımcı olur:

// Every page should have these landmarks
<header>        {/* banner landmark */}
  <nav aria-label="Main">...</nav>
</header>
<main>           {/* main landmark */}
  <h1>Page Title</h1>
  <article>...</article>
  <aside aria-label="Related content">...</aside>
</main>
<footer>         {/* contentinfo landmark */}
  <nav aria-label="Footer">...</nav>
</footer>

Başlık hiyerarşisi mantıksal ve kesintisiz olmalıdır:

// BAD: Skipped heading levels
<h1>Page Title</h1>
<h3>Section</h3>  {/* Skipped h2! */}

// GOOD: Sequential hierarchy
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>

Renk Kontrastı

WCAG 2.1 AA şunları gerektirir:

  • 4,5:1 normal metin için kontrast oranı (18pt / 14pt'nin altında kalın)
  • 3:1 büyük metinler için kontrast oranı (18pt+ / 14pt+ kalın)
  • Kullanıcı arayüzü bileşenleri ve grafik nesneler (düğmeler, simgeler, giriş kenarlıkları) için 3:1
// Tailwind color contrast examples
// FAIL: gray-400 on white (#9ca3af on #fff = 2.8:1)
<p className="text-gray-400">This fails AA</p>

// PASS: gray-700 on white (#374151 on #fff = 10.7:1)
<p className="text-gray-700">This passes AA</p>

// For dark mode, test both themes separately
<p className="text-gray-700 dark:text-gray-300">
  gray-700 on white (10.7:1) / gray-300 on gray-900 (9.2:1)
</p>

Geliştirme sırasında WebAIM Kontrast Denetleyicisini veya tarayıcının DevTools kontrast aracını kullanın. Gerilemeleri yakalamak için bunu Hikaye Kitabınıza veya tasarım belirteçleri sisteminize ekleyin:

// contrast-checker.ts
import { getContrast } from 'polished';

function assertContrast(fg: string, bg: string, level: 'AA' | 'AAA' = 'AA') {
  const ratio = getContrast(fg, bg);
  const required = level === 'AA' ? 4.5 : 7;
  if (ratio < required) {
    throw new Error(
      `Contrast ratio ${ratio.toFixed(2)}:1 fails WCAG ${level} (requires ${required}:1)`
    );
  }
}

Klavye Gezintisi

Her etkileşimli öğeye (bağlantılar, düğmeler, form alanları, özel widget'lar) klavye aracılığıyla erişilebilir ve çalıştırılabilir olmalıdır.

Odak Yönetimi

// Skip link: first element on every page
// Allows keyboard users to jump past navigation
export function SkipLink() {
  return (
    <a
      href="#main-content"
      className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4
                 focus:z-50 focus:px-4 focus:py-2 focus:bg-blue-600 focus:text-white
                 focus:rounded focus:ring-2 focus:ring-white"
    >
      Skip to main content
    </a>
  );
}

// Main content target
<main id="main-content" tabIndex={-1}>
  {/* tabIndex={-1} allows programmatic focus without appearing in tab order */}

Modallarda Odak Yakalama

Bir diyalog açıldığında odak onun içinde sıkışıp kalmalıdır. Kapatıldığında odak tetiğe geri döner:

// focus-trap.tsx using @radix-ui/react-focus-trap (used internally by shadcn Dialog)
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
import { useRef } from 'react';

export function AccessibleModal({ trigger, children, title }: Props) {
  const triggerRef = useRef<HTMLButtonElement>(null);

  return (
    <Dialog>
      <DialogTrigger ref={triggerRef} asChild>
        <button>Open</button>
      </DialogTrigger>
      <DialogContent
        // shadcn Dialog handles focus trap and returns focus to trigger on close
        aria-describedby="dialog-description"
      >
        <DialogTitle>{title}</DialogTitle>
        <p id="dialog-description" className="sr-only">
          {/* Screen reader description of dialog purpose */}
        </p>
        {children}
      </DialogContent>
    </Dialog>
  );
}

Görünür Odak Göstergeleri

WCAG 2.1 SC 2.4.11 (WCAG 2.2'de AA), minimum 2 piksellik bir odak taslağı gerektirir. Değiştirmeden odağı asla bastırmayın:

/* globals.css */
/* NEVER do this: */
:focus { outline: none; }

/* DO this: custom, visible focus ring */
:focus-visible {
  outline: 2px solid hsl(var(--ring));
  outline-offset: 2px;
  border-radius: 4px;
}

/* Remove for mouse users (only show for keyboard) */
:focus:not(:focus-visible) {
  outline: none;
}

ARIA: Ne Zaman ve Nasıl Kullanılır?

ARIA (Erişilebilir Zengin İnternet Uygulamaları) nitelikleri, yardımcı teknolojilerin DOM'u nasıl yorumladığını değiştirir. ARIA'nın ilk kuralı: kullanım durumunuz için yerel bir HTML öğesi mevcutsa onu kullanmayın.

ARIA Etiketleri

// Icon-only button — screen reader has nothing to announce without aria-label
<button aria-label="Close dialog">
  <X className="h-4 w-4" aria-hidden="true" />
</button>

// Form field with visible label — use htmlFor, not aria-label
<label htmlFor="email">Email address</label>
<input id="email" type="email" />

// Input with visible description
<input
  id="password"
  type="password"
  aria-describedby="password-requirements"
/>
<p id="password-requirements">Must be at least 12 characters.</p>

ARIA Canlı Bölgeleri

Odağı kaydırmadan dinamik içerik değişikliklerini duyurun:

// Status messages (search results count, form submission status)
function SearchResults({ count, loading }: Props) {
  return (
    <>
      {/* aria-live="polite" waits for user to finish current action */}
      <div aria-live="polite" aria-atomic="true" className="sr-only">
        {loading ? 'Loading results...' : `${count} results found`}
      </div>
      {/* Visual result count (not for screen readers — aria-hidden) */}
      <span aria-hidden="true">{count} results</span>
    </>
  );
}

// Error messages (aria-live="assertive" interrupts immediately)
function FormError({ error }: { error: string | null }) {
  return (
    <div
      role="alert"
      aria-live="assertive"
      className={cn('text-red-500 text-sm', !error && 'hidden')}
    >
      {error}
    </div>
  );
}

Özel Widget'lar için ARIA

Özel bir widget (sekme paneli, ağaç görünümü, birleşik giriş kutusu) oluşturmanız gerektiğinde, ARIA Yazma Uygulamaları Kılavuzu (APG) kalıplarını tam olarak izleyin:

// Accessible tabs (ARIA tab pattern)
export function Tabs({ items }: { items: Tab[] }) {
  const [active, setActive] = useState(0);

  return (
    <div>
      <div role="tablist" aria-label="Content tabs">
        {items.map((item, i) => (
          <button
            key={item.id}
            role="tab"
            aria-selected={active === i}
            aria-controls={`panel-${item.id}`}
            id={`tab-${item.id}`}
            tabIndex={active === i ? 0 : -1} // Roving tabindex
            onClick={() => setActive(i)}
            onKeyDown={(e) => {
              if (e.key === 'ArrowRight') setActive((active + 1) % items.length);
              if (e.key === 'ArrowLeft') setActive((active - 1 + items.length) % items.length);
            }}
          >
            {item.label}
          </button>
        ))}
      </div>
      {items.map((item, i) => (
        <div
          key={item.id}
          role="tabpanel"
          id={`panel-${item.id}`}
          aria-labelledby={`tab-${item.id}`}
          hidden={active !== i}
        >
          {item.content}
        </div>
      ))}
    </div>
  );
}

Formlar ve Hata İşleme

Erişilebilir formlar, bilişsel ve motor engelleri olan kullanıcılar için en yüksek etkiye sahip iyileştirmeler arasındadır.

// Accessible form field with error state
function TextField({
  id,
  label,
  error,
  required,
  hint,
  ...props
}: TextFieldProps) {
  const hintId = hint ? `${id}-hint` : undefined;
  const errorId = error ? `${id}-error` : undefined;
  const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined;

  return (
    <div>
      <label htmlFor={id} className="font-medium">
        {label}
        {required && <span aria-hidden="true" className="text-red-500 ml-1">*</span>}
        {required && <span className="sr-only">(required)</span>}
      </label>

      {hint && (
        <p id={hintId} className="text-sm text-gray-500 mt-1">
          {hint}
        </p>
      )}

      <input
        id={id}
        aria-required={required}
        aria-invalid={!!error}
        aria-describedby={describedBy}
        className={cn('input', error && 'border-red-500')}
        {...props}
      />

      {error && (
        <p id={errorId} className="text-sm text-red-500 mt-1" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

Görseller ve Medya

// Informative image
<img src="/chart.png" alt="Bar chart showing 40% revenue growth in Q4 2025" />

// Decorative image — empty alt hides it from screen readers
<img src="/divider.png" alt="" role="presentation" />

// Complex image — use aria-describedby for long descriptions
<figure>
  <img
    src="/architecture.png"
    alt="System architecture diagram"
    aria-describedby="arch-desc"
  />
  <figcaption id="arch-desc">
    The diagram shows three tiers: frontend Next.js on port 3000,
    NestJS API on port 3001, and PostgreSQL database on port 5433.
    Redis sits between the API and database layers.
  </figcaption>
</figure>

// SVG icons used as decoration
<svg aria-hidden="true" focusable="false">
  <use href="#icon-search" />
</svg>

Balta çekirdeği ile Otomatik Test

pnpm add -D @axe-core/playwright axe-core
// tests/a11y/homepage.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Homepage accessibility', () => {
  test('should have no WCAG 2.1 AA violations', async ({ page }) => {
    await page.goto('/');

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
      .exclude('#third-party-widget') // Exclude known external violations
      .analyze();

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

  test('should be keyboard navigable', async ({ page }) => {
    await page.goto('/');
    // Tab through interactive elements and verify focus is visible
    await page.keyboard.press('Tab');
    const focusedElement = await page.evaluate(
      () => document.activeElement?.getAttribute('href')
    );
    expect(focusedElement).toBe('#main-content'); // Skip link
  });
});

CI ardışık düzenine ekle:

# .github/workflows/ci.yml
- name: Run accessibility tests
  run: cd apps/web && npx playwright test tests/a11y --reporter=html
- uses: actions/upload-artifact@v4
  with:
    name: a11y-report
    path: apps/web/playwright-report/

Sıkça Sorulan Sorular

WCAG 2.1 A, AA ve AAA arasındaki fark nedir?

A Düzeyi minimumdur; A Düzeyinin başarısız olması, içeriğin bazı kullanıcılar tarafından temel yollarla erişilemeyeceği anlamına gelir. AA Düzeyi çoğu yargı bölgesinde yasal standarttır ve en geniş kullanıcı ihtiyaçlarını hedefler. AAA Düzeyi arzu uyandırıcıdır; bazı kriterler tüm içerik türleri için karşılanamaz. Temeliniz olarak AA uyumluluğunu hedefleyin ve mümkün olduğunda AAA'yı hedefleyin.

shadcn/ui gibi bir bileşen kitaplığı kullanmak uygulamamın erişilebilir olmasını sağlar mı?

shadcn/ui, tasarım gereği erişilebilen Radix UI temel öğeleri üzerine kurulmuştur; bunlar doğru ARIA rollerini, klavye gezinmesini ve odak yönetimini içerir. Ancak yine de anlamlı etiketler eklemeniz, hata durumlarını erişilebilir bir şekilde ele almanız, özel temanızla yeterli renk kontrastını sağlamanız ve gerçek yardımcı teknolojilerle test etmeniz gerekiyor. Bileşen kitaplıkları yükü azaltır ancak erişilebilirlik testi ihtiyacını ortadan kaldırmaz.

Ekran okuyucuyla nasıl test yaparım?

Windows'ta NVDA'yı (ücretsiz) Firefox veya Chrome ile kullanın. MacOS'ta VoiceOver'ı (yerleşik, Cmd+F5) Safari ile kullanın. Mobil cihazlarda TalkBack'i (Android) veya VoiceOver'ı (iOS) kullanın. Anahtar kullanıcı yolculuklarını test edin: form doldurma, modal etkileşimler, yer işaretleri aracılığıyla gezinme ve dinamik içeriği okuma. Ekran okuyucu testi, otomatik araçların gözden kaçırdığı duyuruları, okuma sırasını ve odaklanma davranışını yakalar.

Gezici tabindex modeli nedir?

Gezici tabindex, bileşik widget'lar (sekme listeleri, araç çubukları, radyo grupları, ağaç görünümleri) için klavye desenidir. Grupta aynı anda yalnızca bir öğede tabIndex={0} bulunur; etkin öğe. Diğerlerinin tümü tabIndex={-1} alır. Ok tuşları, odağı grup içinde hareket ettirir ve hangi öğenin tabIndex 0'a sahip olduğunu günceller. Bu, kullanıcının gruptaki her öğede sekme yaparak gezinmesini engeller; gruba Sekme ile girer, Ok tuşlarıyla gezinir ve Sekme ile ayrılır.

AJAX aracılığıyla yüklenen dinamik içerik için erişilebilirliği nasıl yönetirim?

Durum güncellemeleri için aria-live bölgelerini kullanın (arama sonuçları sayılır, onayları kaydeder). Tam sayfa bölümü değişiklikleri için yükleme sonrasında odağı yeni içeriğin başlığına veya kapsayıcısına taşıyın. Yükleme durumları için, güncellenmekte olan bölgede aria-busy="true" ve tamamlandığını duyurmak için aria-live="polite" bölgesini kullanın. Duyuruların net ve zamanında olduğunu doğrulamak için her zaman bir ekran okuyucuyla test edin.


Sonraki Adımlar

Web erişilebilirliği tek seferlik bir denetim değil, sürekli bir uygulamadır. Anlamsal HTML'nizi ve renk kontrastınızı düzelterek başlayın, ardından karmaşık widget'lar için klavye gezintisini ve ARIA'yı katmanlayın ve gerilemeleri yakalamak için CI işlem hattınızda WCAG doğrulamasını otomatikleştirin.

ECOSIRE, her projede temel standart olarak WCAG 2.1 AA uyumlu web uygulamaları oluşturur. Erişilebilirlik denetimine ihtiyacınız varsa veya baştan sona uyumluluk sağlamak istiyorsanız ön uç mühendislik hizmetlerimizi keşfedin.

E

Yazan

ECOSIRE Research and Development Team

ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.

WhatsApp'ta Sohbet Et