React'te Erişilebilirlik (a11y): WCAG Uyumlu Geliştirme

React'te Erişilebilirlik (a11y): WCAG Uyumlu Geliştirme
Web erişilebilirliği, uygulamanızın engelli bireyler dahil herkes tarafından kullanılabilir olmasını sağlar. Türkiye'de Engelliler Hakkında Kanun ve dünyada WCAG (Web Content Accessibility Guidelines) standartları, web uygulamalarının erişilebilir olmasını gerektirmektedir. Bu rehberde, React uygulamalarında erişilebilirliği nasıl sağlayacağınızı pratik örneklerle adım adım ele alacağız.
Erişilebilirlik Neden Önemli?
Dünya nüfusunun yaklaşık %15'i bir tür engelle yaşamaktadır. Erişilebilir web siteleri yalnızca etik bir sorumluluk değil, aynı zamanda daha geniş bir kitleye ulaşma, SEO iyileştirme ve yasal uyumluluk açısından da kritiktir. Ayrıca erişilebilir componentler genellikle daha iyi tasarlanmış ve test edilebilir olur.
Semantik HTML ve ARIA
import { useState, useRef, useEffect, KeyboardEvent } from 'react';
// ❌ KÖTÜ: Div ile yapılmış "buton"
function BadButton() {
return (
<div onClick={() => console.log('click')} style={{ cursor: 'pointer' }}>
Tıkla
</div>
);
// Sorunlar: Keyboard ile erişilemez, screen reader tanımaz, focus olmaz
}
// ✅ İYİ: Semantik HTML
function GoodButton() {
return (
<button onClick={() => console.log('click')} type="button">
Tıkla
</button>
);
// Otomatik: keyboard erişimi, screen reader desteği, focus yönetimi
}
// Erişilebilir Modal Component
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
function AccessibleModal({ isOpen, onClose, title, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
if (isOpen) {
// Mevcut focus'u kaydet
previousFocus.current = document.activeElement as HTMLElement;
// Modal'a focus ver
modalRef.current?.focus();
// Body scroll'u kilitle
document.body.style.overflow = 'hidden';
} else {
// Eski focus'a geri dön
previousFocus.current?.focus();
document.body.style.overflow = '';
}
}, [isOpen]);
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
// Focus trap — Tab ile modal dışına çıkılmasını engelle
if (e.key === 'Tab') {
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements && focusableElements.length > 0) {
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="presentation"
>
{/* Arka plan overlay */}
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
aria-hidden="true"
/>
{/* Modal içeriği */}
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
onKeyDown={handleKeyDown}
className="relative bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
>
<h2 id="modal-title" className="text-xl font-bold mb-4">{title}</h2>
{children}
<button
onClick={onClose}
className="absolute top-4 right-4"
aria-label="Modalı kapat"
>
✕
</button>
</div>
</div>
);
}
<button>, <a>, <nav>, <main>, <article> gibi elementler yerleşik erişilebilirlik özelliklerine sahiptir. ARIA attribute'ları ancak semantik HTML yeterli olmadığında kullanılmalıdır.
Erişilebilir Form Componentleri
interface FormFieldProps {
id: string;
label: string;
error?: string;
helpText?: string;
required?: boolean;
type?: string;
value: string;
onChange: (value: string) => void;
}
function AccessibleFormField({
id, label, error, helpText, required = false, type = 'text', value, onChange,
}: FormFieldProps) {
const errorId = `${id}-error`;
const helpId = `${id}-help`;
// aria-describedby birden fazla id alabilir
const describedBy = [
error ? errorId : null,
helpText ? helpId : null,
].filter(Boolean).join(' ') || undefined;
return (
<div className="space-y-1">
<label htmlFor={id} className="block text-sm font-medium">
{label}
{required && <span className="text-red-500 ml-1" aria-hidden="true">*</span>}
{required && <span className="sr-only">(zorunlu alan)</span>}
</label>
<input
id={id}
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
required={required}
aria-required={required}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={describedBy}
className={`w-full rounded border px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'}`}
/>
{helpText && (
<p id={helpId} className="text-sm text-gray-500">{helpText}</p>
)}
{error && (
<p id={errorId} className="text-sm text-red-500" role="alert">
{error}
</p>
)}
</div>
);
}
Klavye Navigasyonu
import { useState, KeyboardEvent } from 'react';
interface Tab {
id: string;
label: string;
content: React.ReactNode;
}
function AccessibleTabs({ tabs }: { tabs: Tab[] }) {
const [activeIndex, setActiveIndex] = useState(0);
const handleKeyDown = (e: KeyboardEvent, index: number) => {
let newIndex = index;
switch (e.key) {
case 'ArrowRight':
newIndex = (index + 1) % tabs.length;
break;
case 'ArrowLeft':
newIndex = (index - 1 + tabs.length) % tabs.length;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
setActiveIndex(newIndex);
// Yeni tab'a focus ver
document.getElementById(`tab-${tabs[newIndex].id}`)?.focus();
};
return (
<div>
<div role="tablist" aria-label="İçerik sekmeleri" className="flex border-b">
{tabs.map((tab, index) => (
<button
key={tab.id}
id={`tab-${tab.id}`}
role="tab"
aria-selected={activeIndex === index}
aria-controls={`panel-${tab.id}`}
tabIndex={activeIndex === index ? 0 : -1}
onClick={() => setActiveIndex(index)}
onKeyDown={(e) => handleKeyDown(e, index)}
className={`px-4 py-2 font-medium border-b-2 ${
activeIndex === index
? 'border-blue-600 text-blue-600'
: 'border-transparent text-gray-500'
}`}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, index) => (
<div
key={tab.id}
id={`panel-${tab.id}`}
role="tabpanel"
aria-labelledby={`tab-${tab.id}`}
hidden={activeIndex !== index}
tabIndex={0}
className="p-4"
>
{tab.content}
</div>
))}
</div>
);
}
aria-label ve aria-labelledby kullanırken Türkçe karakter desteğine dikkat edin. Screen reader'lar Türkçe karakterleri genellikle doğru okur, ancak test yapmak önemlidir.
Test ve Denetleme Araçları
- eslint-plugin-jsx-a11y: JSX'teki erişilebilirlik sorunlarını lint zamanında yakalar
- axe-core / @axe-core/react: Runtime erişilebilirlik denetimi
- Lighthouse: Chrome DevTools'ta yerleşik erişilebilirlik skoru
- NVDA / VoiceOver: Screen reader ile manuel test
Sonuç
React'te erişilebilirlik, projenin başından itibaren düşünülmesi gereken bir konudur. Semantik HTML kullanın, ARIA attribute'larını doğru uygulayın, klavye navigasyonunu destekleyin, focus yönetimini ihmal etmeyin ve renk kontrastına dikkat edin. Erişilebilirlik sadece engelli kullanıcılar için değil, tüm kullanıcıların deneyimini iyileştiren bir yaklaşımdır.