EduFlow
إتقان CSS الحديثة
⁦0%⁩

الوحدة 3 · متجاوب ومتين

أنماط focus تؤدي عملها

15 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

ابنِ مؤشّرات focus يراها مستخدمو لوحة المفاتيح دائمًا، باستخدام :focus-visible، وحدود outline تصمد في وضع الألوان المفروضة، وscroll padding يُبقي العنصر الذي عليه الـ focus بعيدًا عن رأس الصفحة الثابت.

  1. 1:focus-visible: الحلقة حين تكون مفيدة
  2. 2لماذا يتفوّق الـ outline على box-shadow
  3. 3التباين في الثيمين
  4. 4ضع الـ focus على البطاقة كلها
  5. 5لا تدع الرأس يخفي الـ focus
  6. 6دورك الآن

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

افتح جدول Waypoint، وأبعد الفأرة، واضغط Tab. في النسخة الأولى لن ترى شيئًا. كان ملف إعادة الضبط يحتوي على :focus { outline: none; }، أُضيفت لأن مصمّمًا لم تعجبه الحلقة التي تظهر عند النقر على الأزرار. مستخدمو لوحة المفاتيح، ومستخدمو أجهزة التبديل (switch)، وكثير من مستخدمي مكبّرات الشاشة يتنقّلون عبر الـ focus، وكانوا يتنقّلون معصوبي الأعين.

إزالة أنماط الـ focus ليست خيارًا جماليًا؛ إنها صفحة معطوبة لمجموعة كبيرة من الناس. والخبر الجيد أن CSS الحديثة تجعل كتابة أنماط focus جيدة أمرًا سهلًا.

:focus-visible: الحلقة حين تكون مفيدة

شكوى المصمّم كانت حقيقية: حلقة بعد كل نقرة فأرة تبدو خللًا. و:focus-visible تحلّ ذلك. إنها لا تطابق العنصر الذي عليه الـ focus إلا حين يرى المتصفّح أن المؤشّر مفيد:

  • وصل الـ focus عبر لوحة المفاتيح: نعم.
  • نقرة فأرة وضعت الـ focus على زر أو رابط: لا.
  • استقبل حقل نصي الـ focus، بأيّ طريقة: نعم، لأنك تحتاج إلى رؤية المكان الذي تكتب فيه.
focus.css
:root {
  --focus: light-dark(#6d28d9, #c4b5fd);
}

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

هذه القاعدة الواحدة تصلح ما أفسده ملف إعادة الضبط. إنها تنطبق على كل عنصر يمكن أن يستقبل الـ focus، فيحصل مستخدمو لوحة المفاتيح على حلقة واضحة، ولا يراها مستخدمو الفأرة على الأزرار. و:focus-visible متاحة على نطاق واسع حسب Baseline.

لماذا يتفوّق الـ outline على box-shadow

كثير من أنظمة التصميم ترسم حلقات الـ focus بـ box-shadow لأن الظلال كانت تتبع الزوايا المستديرة بينما الـ outlines لم تكن تفعل. لكن الـ outlines تتبع border-radius في كل المتصفّحات الحالية منذ 2023، ولها ثلاث مزايا:

  1. لا إزاحة في التخطيط. الـ outlines لا تشغل مساحة، لذا فإن وضع الـ focus على زر لا يدفع جيرانه أبدًا.
  2. outline-offset تضع فجوة بين العنصر والحلقة، فتبقى الحلقة ظاهرة على خلفية العنصر نفسه.
  3. وضع الألوان المفروضة. ثيمات التباين العالي في Windows تزيل box-shadow كليًا لكنها تُبقي الـ outlines، بعد إعادة تلوينها بلون من ألوان النظام.

التباين في الثيمين

يحتاج مؤشّر الـ focus إلى تباين لا يقل عن 3:1 مع ما يحيط به (معيار تباين العناصر غير النصية في WCAG). والبنفسجي الذي يعمل على الأبيض قد يختفي على سطح رمادي داكن. تعريف --focus بـ light-dark()، كما في الأعلى، يعطي كل ثيم لون حلقته الخاص من token واحد: بنفسجي عميق على الأسطح الفاتحة، وخزامي باهت على الداكنة.

وللـ components التي تجلس على خلفيات لا يمكن توقّعها (صورة الـ hero، أو شارة مسار ملوّنة)، الحلقة ثنائية اللون هي الأكثر أمانًا: outline بلون الـ focus مع هالة متباينة داخلها مباشرةً:

CSS
.hero :focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 2px var(--surface);  /* halo between element and ring */
}

الظلّ هنا زخرفة فوق outline حقيقي، لذا يظل مستخدمو الألوان المفروضة يحصلون على الـ outline.

ضع الـ focus على البطاقة كلها

بطاقات الجلسات في Waypoint قابلة للنقر: العنوان رابط، ممدود بواسطة pseudo-element ليغطّي البطاقة. عندها تظهر الحلقة حول نص العنوان فقط، وهذا يبدو غريبًا. انقلها إلى البطاقة بـ :has():

CSS
.session-card:has(a:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}
.session-card a:focus-visible {
  outline: none;   /* the card shows the ring instead */
}

إزالة حلقة الرابط نفسه مقبولة هنا، لأن حلقة البطاقة تحلّ محلّها كلما كان على الرابط focus ظاهر. وهذا هو النوع الوحيد من outline: none الذي ينبغي أن تطلقه: نوعٌ يغطّيه مؤشّر آخر.

لا تدع الرأس يخفي الـ focus

أضافت WCAG 2.2 معيار Focus Not Obscured (الـ focus غير محجوب): لا يجوز أن يختفي العنصر الذي عليه الـ focus كليًا خلف محتوى وضعه المطوّر فوقه، مثل رأس صفحة ثابت أو شريط ملفات تعريف الارتباط (cookies). رأس Waypoint ثابت وارتفاعه 64px، وحين يتنقّل المستخدم إلى الخلف، يمرّر المتصفّح البطاقة التي عليها الـ focus إلى أعلى الـ viewport، تحت الرأس مباشرةً.

CSS
html {
  scroll-padding-top: 64px;
}

الـ scroll padding تخبر المتصفّح بأن جزءًا من مساحة العرض في حاوية التمرير مغطّى. والتمرير الناتج عن الـ focus والروابط الداخلية (anchors) وscrollIntoView() كلها تحترمها. وإن تغيّر ارتفاع الرأس، فخزّنه في custom property واستخدمها في المكانين.

دورك الآن

في صفحة التمرين ملف إعادة الضبط السيئ. استبدله بحلقة :focus-visible، وانقل الحلقة إلى البطاقة حين يكون الـ focus على رابطها، وأضف scroll padding للرأس ذي الـ 64px. وبهذا ينتهي القسم المتجاوب؛ وبعده ستضيف الحركة، بدءًا بالـ transitions والـ keyframes واحترام إعدادات تقليل الحركة.

أهم النقاط

  • :focus-visible تطابق حين يقرّر المتصفّح أن مؤشّر الـ focus مفيد، وهذا عادةً عند التنقّل بلوحة المفاتيح، فتستطيع إظهار حلقة واضحة دون إظهارها مع كل نقرة فأرة.
  • استخدم outline مع outline-offset لحلقات الـ focus؛ فهي لا تغيّر التخطيط، وتتبع border-radius، وتبقى ظاهرة في وضع الألوان المفروضة (forced colors) في Windows حيث تختفي box-shadow.
  • عرّف لون الـ focus بـ light-dark() ليحصل كل ثيم على لون حلقة خاص به، ثم تحقّق من أن تباينه مع السطح لا يقل عن 3:1 في الثيمين.
  • scroll-padding-top على حاوية التمرير تمنع رأس الصفحة الثابت (sticky) من تغطية العنصر الذي استقبل الـ focus للتوّ.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

لماذا يُعدّ button:focus { outline: none; box-shadow: 0 0 0 3px violet; } نمط focus محفوفًا بالمخاطر؟

السؤال 2 من 3

تريد حلقة focus عند التنقّل بلوحة المفاتيح، لا حين ينقر مستخدم الفأرة زرًّا. أيّ selector تستخدم؟

السؤال 3 من 3

رأس صفحة Waypoint بـ position: sticky وارتفاعه 64px. حين يتنقّل المستخدمون إلى الخلف عبر الجدول بلوحة المفاتيح، تُمرَّر البطاقة التي عليها الـ focus إلى مجال الرؤية لكنها تختبئ تحت الرأس. ما الإصلاح؟

تدريب

بيئة تجريب الكود

أعد حلقة الـ focus

ملف إعادة الضبط في النموذج الأولي يزيل كل outline للـ focus. استبدله بحيث:

  1. يحصل أيّ عنصر عليه focus ظاهر على outline بقيمة 2px solid بلون var(--focus) مع outline-offset لا تقل عن 2px (ولا حلقة حين لا يكون عليه focus).
  2. حين يكون على رابط البطاقة focus ظاهر، تُظهر .session-card الحلقة نفسها (يمكنك إخفاء حلقة الرابط نفسه في هذه الحالة).
  3. يتوقّف التمرير الناتج عن الـ focus تحت رأس الصفحة الثابت ذي الـ 64px.
<header class="site-header">Waypoint Conf 2026 <button type="button" id="tickets">Get tickets</button></header>
<main>
  <article class="session-card">
    <h3><a href="#s1" id="card-link">Focus Styles That Work</a></h3>
    <p>Rings, offsets and forced colors.</p>
  </article>
</main>جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

المعاينة

الـ console

(لا مخرجات)

الفحوص

  • الزر الذي عليه الـ focus يحصل على حلقة 2px solid بلون الـ focus
  • للحلقة إزاحة لا تقل عن 2px
  • لا حلقة حين لا يكون هناك focus
  • تُظهر البطاقة الحلقة حين يكون الـ focus على رابطها
  • التمرير الناتج عن الـ focus يتجاوز الرأس ذا الـ 64px