أداء الويب وإمكانية الوصول
معايير إمكانية الوصول والاختبار
إمكانية الوصول (a11y) لم تعد "شيئًا جميلاً" — إنها متطلب قانوني في العديد من الولايات القضائية ومجال تقييم أساسي في مقابلات الواجهات الأمامية.
صار WCAG 2.2 توصيةً من W3C أول مرة في 5 أكتوبر 2023، ثم أُعيد نشره كتوصية مُنقَّحة في 12 ديسمبر 2024. هذا التنقيح مهم في المقابلات، لأنه أزال معيار نجاح بدل أن يضيف واحدًا: 4.1.1 التحليل (Parsing) لم يعد موجودًا، بحجة أن مشكلات التحليل التي وصفها لم تعد تحدث في المتصفحات الحديثة. فإن كنت لا تزال تدقّق مقابل 4.1.1 فأنت تدقّق مقابل معيار متقاعد — رغم أن المشاريع الملتزمة تعاقديًا بـ WCAG 2.0 أو 2.1 قد يظل مطلوبًا منها اختباره. راجع التوصية الحالية بدل أي ملخص؛ فالإرشادات تُنقَّح في مكانها والملخصات لا تتبعها.
WCAG 2.2 AA — ما تحتاج معرفته
يُنظم WCAG في أربعة مبادئ: قابل للإدراك، قابل للتشغيل، مفهوم، متين (POUR). تستهدف معظم الشركات مطابقة المستوى AA كمعيار.
المعايير الجديدة الرئيسية في WCAG 2.2
2.5.8 حجم الهدف (الحد الأدنى) — المستوى AA
يجب أن تكون أهداف التفاعل بحجم 24x24 بكسل CSS على الأقل:
/* ضمان أن جميع العناصر التفاعلية تحقق الحد الأدنى لحجم الهدف */
button, a, input[type="checkbox"], input[type="radio"], select {
min-width: 24px;
min-height: 24px;
}
/* أفضل: استخدم أهداف لمس سخية للأجهزة المحمولة */
.btn {
min-width: 44px; /* توصية Apple HIG */
min-height: 44px;
padding: 12px 16px;
}
/* الروابط المضمنة مُعفاة إذا حققت متطلبات تباعد النص */
2.4.11 عدم إخفاء التركيز (الحد الأدنى) — المستوى AA
عندما يحصل عنصر على تركيز لوحة المفاتيح، يجب ألا يكون مخفيًا بالكامل بواسطة رؤوس لاصقة أو تذييلات أو نوافذ منبثقة أو محتوى متداخل آخر:
/* سيئ: الرأس اللاصق يمكن أن يغطي العناصر المُركزة */
.header {
position: sticky;
top: 0;
z-index: 100;
}
/* جيد: مراعاة الرأس اللاصق عند التمرير للعناصر المُركزة */
:target {
scroll-margin-top: 80px; /* ارتفاع الرأس اللاصق */
}
/* ضمان تمرير العناصر المُركزة للمنطقة المرئية */
*:focus {
scroll-margin-top: 80px;
scroll-margin-bottom: 60px;
}
3.3.7 الإدخال المكرر — المستوى A
لا تُجبر المستخدمين على إعادة إدخال معلومات قدموها بالفعل في نفس الجلسة:
// سيئ: طلب عنوان الشحن بعد أن أدخل المستخدم عنوان الفوترة
function CheckoutForm() {
return (
<form>
<BillingAddressFields />
{/* يُجبر المستخدم على إعادة كتابة كل شيء */}
<ShippingAddressFields />
</form>
);
}
// جيد: عرض إعادة استخدام البيانات المُدخلة سابقًا
function CheckoutForm() {
const [sameAsBilling, setSameAsBilling] = useState(true);
return (
<form>
<BillingAddressFields />
<label>
<input
type="checkbox"
checked={sameAsBilling}
onChange={(e) => setSameAsBilling(e.target.checked)}
/>
عنوان الشحن نفس عنوان الفوترة
</label>
{!sameAsBilling && <ShippingAddressFields />}
</form>
);
}
3.3.8 المصادقة المتاحة (الحد الأدنى) — المستوى AA
يجب ألا تعتمد المصادقة على اختبارات الوظائف المعرفية (مثل تذكر كلمة مرور أو حل لغز) بدون توفير بدائل:
// جيد: توفير طرق مصادقة متعددة
function LoginPage() {
return (
<div>
<h1>تسجيل الدخول</h1>
{/* السماح لمديري كلمات المرور بالملء التلقائي */}
<input type="email" autoComplete="email" />
<input type="password" autoComplete="current-password" />
{/* بديل: مفتاح المرور/القياسات الحيوية */}
<button onClick={handlePasskeyLogin}>تسجيل الدخول بمفتاح المرور</button>
{/* بديل: رابط سحري */}
<button onClick={handleMagicLink}>أرسل لي رابط تسجيل دخول</button>
{/* إذا استُخدم CAPTCHA، وفّر بديلاً متاحًا */}
</div>
);
}
أدوار ومعرّفات وخصائص ARIA
القاعدة الأولى لـ ARIA: لا تستخدم ARIA إذا كان بإمكانك استخدام HTML الدلالي بدلاً من ذلك. والقاعدة الثانية الأقل اقتباسًا أن ARIA السيئ أسوأ من غيابه — فالـ role الخاطئ يتجاوز دلالة العنصر الحقيقية ويضلّل قارئ الشاشة فعليًا، بينما <div> مجرد لا يقول شيئًا فحسب.
هل تحتاج ARIA هنا، أم عنصرًا مختلفًا؟
ماذا يُفترض أن يفعل هذا العنصر؟
HTML الدلالي مقابل ARIA
<!-- سيئ: استخدام ARIA لإعادة إنشاء ما يوفره HTML بالفعل -->
<div role="button" tabindex="0" aria-pressed="false"
onclick="handleClick()" onkeydown="handleKeydown(event)">
انقر هنا
</div>
<!-- جيد: استخدم العنصر الأصلي — لديه معالجة لوحة مفاتيح مدمجة
وإدارة تركيز ودعم قارئ شاشة -->
<button onclick="handleClick()">انقر هنا</button>
متى يكون ARIA ضروريًا
<!-- صندوق مركب مخصص — لا يوجد مكافئ HTML أصلي.
role="combobox" يوضع على عنصر INPUT. شكل الـ div المُغلِّف
مع aria-owns هو نمط ARIA 1.1؛ و aria-owns مهجور لصالح
aria-controls. -->
<div>
<input type="text" role="combobox" aria-expanded="true"
aria-autocomplete="list" aria-label="بحث"
aria-controls="results-list" aria-activedescendant="option-3">
</div>
<ul id="results-list" role="listbox">
<li id="option-1" role="option">النتيجة 1</li>
<li id="option-2" role="option">النتيجة 2</li>
<li id="option-3" role="option" aria-selected="true">النتيجة 3</li>
</ul>
<!-- منطقة حية لتحديثات المحتوى الديناميكي -->
<div aria-live="polite" aria-atomic="true">
تم العثور على 3 نتائج
</div>
<!-- مؤشر تقدم -->
<div role="progressbar" aria-valuenow="65" aria-valuemin="0"
aria-valuemax="100" aria-label="تقدم الرفع">
65%
</div>
أنماط التنقل بلوحة المفاتيح
حبس التركيز في النوافذ المنبثقة
عند فتح نافذة منبثقة، يجب أن يبقى التركيز داخلها حتى تُغلق:
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
// حفظ العنصر الذي كان مُركزًا قبل فتح النافذة
previousFocusRef.current = document.activeElement;
const modal = modalRef.current;
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
// تركيز العنصر الأول في النافذة
firstFocusable?.focus();
function handleKeyDown(e) {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key !== 'Tab') return;
// حبس التركيز داخل النافذة
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
modal.addEventListener('keydown', handleKeyDown);
return () => {
modal.removeEventListener('keydown', handleKeyDown);
// استعادة التركيز عند إغلاق النافذة
previousFocusRef.current?.focus();
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div ref={modalRef} role="dialog" aria-modal="true"
aria-labelledby="modal-title" onClick={(e) => e.stopPropagation()}>
{children}
</div>
</div>
);
}
إعلانات تغيير المسار
يجب على تطبيقات الصفحة الواحدة إعلان التنقل لقارئات الشاشة:
function RouteAnnouncer() {
const location = useLocation();
const [announcement, setAnnouncement] = useState('');
useEffect(() => {
// إعلان عنوان الصفحة الجديدة لقارئات الشاشة
const pageTitle = document.title;
setAnnouncement(`تم الانتقال إلى ${pageTitle}`);
}, [location]);
return (
<div
role="status"
aria-live="assertive"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
);
}
روابط التخطي
السماح لمستخدمي لوحة المفاتيح بتخطي التنقل المتكرر:
<!-- أول عنصر قابل للتركيز في الصفحة -->
<a href="#main-content" class="skip-link">تخطي إلى المحتوى الرئيسي</a>
<nav><!-- تنقل طويل --></nav>
<main id="main-content" tabindex="-1">
<!-- محتوى الصفحة -->
</main>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 1000;
transition: top 0.2s;
}
.skip-link:focus {
top: 0; /* مرئي فقط عند التركيز عبر لوحة المفاتيح */
}
</style>
متطلبات تباين الألوان
| نوع النص | الحد الأدنى لنسبة التباين (AA) |
|---|---|
| نص عادي (< 18 نقطة أو < 14 نقطة عريض) | 4.5:1 |
| نص كبير (≥ 18 نقطة أو ≥ 14 نقطة عريض) | 3:1 |
| مكونات واجهة المستخدم والكائنات الرسومية | 3:1 |
/* جيد: تركيبات عالية التباين */
.text-primary { color: #1a1a1a; } /* على الأبيض: 17.4:1 */
.text-secondary { color: #595959; } /* على الأبيض: 7.0:1 */
/* سيئ: تباين غير كافٍ */
.text-light { color: #aaaaaa; } /* على الأبيض: 2.3:1 — يفشل */
تفضيلات الحركة ونظام الألوان
prefers-reduced-motion
احترم المستخدمين الحساسين للحركة:
/* الافتراضي: الرسوم المتحركة مفعّلة */
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: scale(1.05);
}
/* تقليل أو إزالة الحركة للمستخدمين الذين يفضلون ذلك */
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
.card:hover {
transform: none;
}
/* استبدال الرسوم المتحركة بتغييرات الشفافية */
.fade-in {
animation: none;
opacity: 1;
}
}
prefers-color-scheme
دعم تفضيل الوضع الداكن للنظام:
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--border-color: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #f0f0f0;
--border-color: #333333;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
أساسيات اختبار قارئ الشاشة
VoiceOver (macOS)
| الإجراء | الاختصار |
|---|---|
| تشغيل/إيقاف | Cmd + F5 |
| قراءة العنصر التالي | VO + السهم الأيمن (VO = Ctrl + Option) |
| التفاعل مع العنصر | VO + Shift + السهم لأسفل |
| إيقاف القراءة | Ctrl |
| فتح الدوّار (المعالم والعناوين) | VO + U |
NVDA (Windows)
| الإجراء | الاختصار |
|---|---|
| تشغيل/إيقاف | Ctrl + Alt + N |
| قراءة العنصر التالي | السهم لأسفل |
| قائمة العناوين | H (أو Insert + F7) |
| قائمة المعالم | D |
| إيقاف القراءة | Ctrl |
شجرة إمكانية الوصول في Chrome DevTools
- افتح DevTools → تبويب Elements
- انقر على لوحة "Accessibility" في الشريط الجانبي
- تفقد أي عنصر لرؤية:
- الدور: نوع العنصر الذي يراه قارئ الشاشة
- الاسم: ما يُعلنه قارئ الشاشة (من label أو aria-label أو alt)
- الحالة: مُحدد، موسّع، مُختار، إلخ.
- فعّل "Full accessibility tree" في إعدادات DevTools لاستبدال عرض شجرة DOM
نصيحة للمقابلات: قدّم إمكانية الوصول كاهتمام هندسي أساسي لا كفكرة لاحقة — وانتبه للإحصائية التي يقتبسها الجميع. الرقم الصناعي القديم للاختبار الآلي هو 20-30% من المشكلات، لكن تحليل Deque عام 2021 لنحو 2000 تدقيق وجد أن أدوات axe الخاصة بها غطّت 57% بالكامل من حيث حجم المشكلات المكتشفة. الرقمان يقيسان أشياء مختلفة (أنواع المشكلات مقابل عددها، وأدوات مختلفة)، وهذه هي الإجابة الصادقة: الاختبار الآلي يلتقط حصة كبيرة ومتنامية، ولن يلتقط أبدًا ترتيب قراءة خاطئًا أو تسمية مضللة، والاختبار اليدوي بلوحة المفاتيح وقارئ الشاشة ليس اختياريًا. تسمية هذا الالتباس يقع موقعًا أفضل من اقتباس أي من الرقمين كحقيقة.
تنتهي الوحدة 5 هنا. الاختبار يغطي الأداء وإمكانية الوصول معًا، بما في ذلك أرقام العتبات — وهي التي يخسر الناس عليها الدرجات. :::
سجّل الدخول للتقييم