Compliance & Regulation serimizin bir parçası
Tam kılavuzu okuyunWeb 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.
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.
İlgili Makaleler
Audit Preparation Checklist: Getting Your Books Ready
Complete audit preparation checklist covering financial statement readiness, supporting documentation, internal controls documentation, auditor PBC lists, and common audit findings.
Australian GST Guide for eCommerce Businesses
Complete Australian GST guide for eCommerce businesses covering ATO registration, the $75,000 threshold, low value imports, BAS lodgement, and GST for digital services.
Canadian HST/GST Guide: Province-by-Province
Complete Canadian HST/GST guide covering registration requirements, province-by-province rates, input tax credits, QST, place of supply rules, and CRA compliance.
Compliance & Regulation serisinden daha fazlası
Audit Preparation Checklist: Getting Your Books Ready
Complete audit preparation checklist covering financial statement readiness, supporting documentation, internal controls documentation, auditor PBC lists, and common audit findings.
Australian GST Guide for eCommerce Businesses
Complete Australian GST guide for eCommerce businesses covering ATO registration, the $75,000 threshold, low value imports, BAS lodgement, and GST for digital services.
Canadian HST/GST Guide: Province-by-Province
Complete Canadian HST/GST guide covering registration requirements, province-by-province rates, input tax credits, QST, place of supply rules, and CRA compliance.
Healthcare Accounting: Compliance and Financial Management
Complete guide to healthcare accounting covering HIPAA financial compliance, contractual adjustments, charity care, cost report preparation, and revenue cycle management.
India GST Compliance for Digital Businesses
Complete India GST compliance guide for digital businesses covering registration, GSTIN, rates, input tax credits, e-invoicing, GSTR returns, and TDS/TCS provisions.
Fund Accounting for Nonprofits: Best Practices
Master nonprofit fund accounting with net asset classifications, grant tracking, Form 990 preparation, functional expense allocation, and audit readiness best practices.