إنشاء مكوِّن تبديل المظهر

نظرة عامة أساسية حول كيفية إنشاء مكوِّن لتبديل المظاهر تكيُّفي ويسهل الوصول إليه

في هذه المشاركة، أرغب في مشاركة التفكير حول طريقة لإنشاء مكون تبديل المظهر الداكن والفاتح. جرِّب الإصدار التجريبي.

العرض التوضيحي لتسهيل مستوى الرؤية

في ما يلي إصدار YouTube من هذه المشاركة إذا كنت تفضّل الفيديوهات:

نظرة عامة

قد يوفر أحد مواقع الويب إعدادات للتحكم في نظام الألوان بدلاً من الاعتماد بالكامل على تفضيل النظام. هذا يعني أن المستخدمين قد يتصفحون في وضع آخر غير تفضيلات النظام. على سبيل المثال، يكون نظام المستخدم بمظهر فاتح، لكن المستخدم يفضل عرض موقع الويب في المظهر الداكن.

هناك عدد من اعتبارات هندسة الويب عند إنشاء هذه الميزة. على سبيل المثال، يجب ضبط المتصفّح للخيار المفضَّل في أقرب وقت ممكن لمنع وميض ألوان الصفحة، ويجب مزامنة عنصر التحكّم مع النظام أولاً، ثم السماح بالاستثناءات المخزّنة من جهة العميل.

يعرِض المخطّط البياني معاينة لتحميل صفحة JavaScript وأحداث التفاعل في المستندات لتوضيح أنّ هناك 4 مسارات لضبط المظهر بشكلٍ عام.

Markup

يجب استخدام <button> لمفتاح التبديل، حيث يمكنك الاستفادة من أحداث التفاعل والميزات التي يوفّرها المتصفّح، مثل أحداث النقر وقابلية التركيز.

الزر

يحتاج الزر إلى فئة للاستخدام من CSS ومعرّف للاستخدام من JavaScript. بالإضافة إلى ذلك، بما أنّ محتوى الزر هو رمز وليس كنص، أضِف السمة title لتقديم معلومات عن الغرض من الزرّ. أخيرًا، أضِف [aria-label] للاحتفاظ بحالة زر الرمز، حتى تتمكن برامج قراءة الشاشة من مشاركة حالة المظهر مع الأشخاص ذوي العجز البصري.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto"
>
  …
</button>

aria-label وaria-live مهذبين

للإشارة إلى برامج قراءة الشاشة بضرورة الإعلان عن التغييرات التي تطرأ على aria-label، أضِف aria-live="polite" إلى الزر.

<button 
  class="theme-toggle" 
  id="theme-toggle" 
  title="Toggles light & dark" 
  aria-label="auto" 
  aria-live="polite"
>
  …
</button>

تشير إضافة الترميز هذه إلى برامج قراءة الشاشة، بدلاً من استخدام aria-live="assertive"، لإعلام المستخدم بما تم إجراؤه. في حال النقر على هذا الزر، سيشير إلى اسم "فاتح" أو "داكن" بناءً على ما أصبح عليه aria-label.

رمز الرسم المتجه القابل للتطوير (SVG)

يوفر SVG طريقة لإنشاء أشكال عالية الجودة وقابلة للتطوير باستخدام أقل قدر من الترميز. يمكن أن يؤدي التفاعل مع الزر إلى تشغيل حالات مرئية جديدة للمتجهات، مما يجعل SVG رائعًا للأيقونات.

يظهر ترميز SVG التالي ضمن <button>:

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  …
</svg>

تمت إضافة aria-hidden إلى عنصر SVG حتى تعرف برامج قراءة الشاشة تجاهله لأنّه مصنَّف كعرض تقديمي. يُعد هذا أمرًا رائعًا للقيام بالزينة المرئية، مثل الأيقونة داخل الزر. بالإضافة إلى سمة viewBox المطلوبة على العنصر، أضِف الارتفاع والعرض لأسباب مشابهة لأن الصور يجب أن تكون بحجم مضمّن.

الشمس

رمز الشمس المعروض مع تلاشي أشعة الشمس وسهم زهري اللون يشير إلى الدائرة في المنتصف.

يتكون رسم الشمس من دائرة وخطوط يسهل رسمها SVG. يتم توسيط <circle> من خلال ضبط الخاصيتين cx وcy على 12، وهو نصف حجم إطار العرض (24)، ثم يتم تحديد نصف قطر (r) بقيمة 6 الذي يضبط الحجم.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>

بالإضافة إلى ذلك، تشير خاصية القناع إلى رقم تعريف عنصر SVG الذي ستنشئه بعد ذلك، وأخيرًا لون تعبئة يتطابق مع لون نص الصفحة مع currentColor.

أشعة الشمس

رمز الشمس الذي يظهر فيه مركز الشمس تلاشى، وسهمًا بلون زهري اللون يشير إلى شعاع الشمس.

بعد ذلك، تتم إضافة خطوط شعاع الشمس أسفل الدائرة مباشرةً، داخل مجموعة من عناصر المجموعة <g>.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    <line x1="12" y1="1" x2="12" y2="3" />
    <line x1="12" y1="21" x2="12" y2="23" />
    <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
    <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
    <line x1="1" y1="12" x2="3" y2="12" />
    <line x1="21" y1="12" x2="23" y2="12" />
    <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
    <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
  </g>
</svg>

هذه المرة، بدلاً من ضبط قيمة fill على currentColor، يتم ضبط الحد الخارجي لكل سطر. الخطوط بالإضافة إلى الأشكال الدائرية تخلق شمسًا جميلة مع عوارض.

القمر

لتصوير الانتقال السلس بين الضوء (الشمس) والظلام (القمر)، يمثّل القمر زيادة في أيقونة الشمس، وذلك باستخدام قناع SVG.

<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
  <circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
  <g class="sun-beams" stroke="currentColor">
    …
  </g>
  <mask class="moon" id="moon-mask">
    <rect x="0" y="0" width="100%" height="100%" fill="white" />
    <circle cx="24" cy="10" r="6" fill="black" />
  </mask>
</svg>
رسم من ثلاث طبقات رأسية للمساعدة في توضيح طريقة تطبيق الإخفاء الطبقة العلوية عبارة عن مربع
أبيض بدائرة سوداء. الطبقة الوسطى هي أيقونة الشمس.
تتم تسمية الطبقة السفلية على أنها النتيجة وتعرض رمز الشمس
بقطعة حيث توجد الدائرة السوداء في الطبقة العلوية.

تعتبر القناعات ذات الرسومات الموجّهة التي يمكن تغيير حجمها (SVG) فعالة، ما يسمح للألوان الأبيض والأسود بإزالة أجزاء من رسم آخر أو تضمينها. سيغطي رمز الشمس شكل القمر <circle> مع قناع SVG، وذلك من خلال تحريك شكل دائري داخل المساحة المخصّصة للقناع وخارجه.

ماذا يحدث إذا تعذّر تحميل خدمة CSS؟

لقطة شاشة لزر متصفّح عادي يظهر فيه رمز الشمس.

قد يكون من الجيد اختبار ملف SVG كما لو لم يتم تحميل CSS للتأكد من أنّ النتيجة ليست كبيرة الحجم أو لا تسبب مشاكل في التنسيق. إنّ سمتَي الارتفاع والعرض المضمّن في رسومات موجّهة يمكن تغيير حجمها (SVG) بالإضافة إلى استخدام currentColor تساعد في الحد الأدنى من قواعد النمط التي يمكن أن يستخدمها المتصفّح في حال عدم تحميل CSS. وهذا من شأنه إنشاء أنماط دفاعية جيدة ضد اضطراب الشبكة.

التنسيق

تحتوي مكوِّن تبديل المظاهر على مساحة صغيرة على السطح، لذا لست بحاجة إلى شبكة أو flexbox للتنسيق. وبدلاً من ذلك، يتم استخدام تحديد موضع SVG وعمليات تحويل CSS.

الأنماط

.theme-toggle نمط

العنصر <button> هو حاوية لأشكال الرموز وأنماطها. سيحمل السياق الأصلي هذا ألوانًا وأحجامًا تكيُّفيًا لتمريرها إلى SVG.

المهمة الأولى هي جعل الزر دائرة وإزالة أنماط الزر الافتراضية:

.theme-toggle {
  --size: 2rem;
  
  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;
}

بعد ذلك، أضف بعض أنماط التفاعل. إضافة نمط مؤشر لمستخدمي الماوس. يمكنك إضافة touch-action: manipulation للحصول على تجربة لمس سريعة الاستجابة. يمكنك إزالة التمييز شبه الشفاف الذي ينطبق عليه نظام التشغيل iOS على الأزرار. أخيرًا، أعط حالة التركيز التي تحدد بعض المساحة للتنفس من حافة العنصر:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;
}

يحتاج ملف SVG الموجود داخل الزر إلى بعض الأنماط أيضًا. يجب أن يناسب الرسومات الموجّهة التي يمكن تغيير حجمها (SVG) حجم الزرّ، ولإضفاء نعومة بصرية، عليك تقريب نهايات الخط على النحو التالي:

.theme-toggle {
  --size: 2rem;

  background: none;
  border: none;
  padding: 0;

  inline-size: var(--size);
  block-size: var(--size);
  aspect-ratio: 1;
  border-radius: 50%;

  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  outline-offset: 5px;

  & > svg {
    inline-size: 100%;
    block-size: 100%;
    stroke-linecap: round;
  }
}

تغيير الحجم التكيُّفي مع الاستعلام عن الوسائط في hover

حجم زر الرمز صغير بعض الشيء عند 2rem، وهو أمر مناسب لمستخدمي الماوس، إلا أنّه قد يمثّل صعوبة في التعامل مع المؤشر التقريبي مثل الإصبع. جعل الزر يفي بالعديد من إرشادات حجم اللمس باستخدام طلب بحث وسائط التمرير لتحديد زيادة في الحجم.

.theme-toggle {
  --size: 2rem;
  …
  
  @media (hover: none) {
    --size: 48px;
  }
}

أنماط SVG للشمس والقمر

يحمل الزر الجوانب التفاعلية لمكوِّن تبديل المظهر بينما يضم رسومات موجّهة يمكن تغيير حجمها (SVG) بداخله الجوانب المرئية والمتحركة. هذا هو المكان الذي يمكن فيه جعل الأيقونة جميلة وجعلها تنبض بالحياة.

مظهر فاتح

ALT_TEXT_HERE

لكي تحدث الصور المتحركة على شكل صور متحركة وتدويرها من مركز أشكال SVG، عليك ضبط transform-origin: center center. تُستخدم الألوان المتكيفة التي يوفرها الزر هنا من خلال الأشكال. يستخدم القمر والشمس الزرّين var(--icon-fill) وvar(--icon-fill-hover) لملء بياناتهما، في حين تستخدم أشعة الشمس متغيرات السكتة الدماغية.

.sun-and-moon {
  & > :is(.moon, .sun, .sun-beams) {
    transform-origin: center center;
  }

  & > :is(.moon, .sun) {
    fill: var(--icon-fill);

    @nest .theme-toggle:is(:hover, :focus-visible) > & {
      fill: var(--icon-fill-hover);
    }
  }

  & > .sun-beams {
    stroke: var(--icon-fill);
    stroke-width: 2px;

    @nest .theme-toggle:is(:hover, :focus-visible) & {
      stroke: var(--icon-fill-hover);
    }
  }
}

مظهر داكن

ALT_TEXT_HERE

يجب أن تزيل أنماط القمر شعاع الشمس، وتكبير دائرة الشمس، وتحريك قناع الدائرة.

.sun-and-moon {
  @nest [data-theme="dark"] & {
    & > .sun {
      transform: scale(1.75);
    }

    & > .sun-beams {
      opacity: 0;
    }

    & > .moon > circle {
      transform: translateX(-7px);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
      }
    }
  }
}

لاحظ أن المظهر الداكن لا يحتوي على أي تغيرات أو انتقالات في الألوان. يمتلك مكون الزر الرئيسي الألوان، حيث إنها تتكيف بالفعل داخل سياق مظلم وفاتح. يجب أن تكون معلومات الانتقال وراء استعلام وسائط تفضيل الحركة للمستخدم.

Animation

يجب أن يكون الزرّ فعالاً وعفويًا ولكن بدون انتقالات في هذه المرحلة. تتناول الأقسام التالية كيفية تحديد كيفية وماذا التحولات.

مشاركة الاستعلامات عن الوسائط واستيراد التخفيفات

لتسهيل وضع الانتقالات والرسوم المتحركة خلف الإعدادات المفضّلة للحركة في نظام تشغيل المستخدم، يتيح المكوِّن الإضافي PostCSS الوسائط المخصصة استخدام مواصفات CSS الاختبارية لمتغيرات استعلامات الوسائط:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

/* usage example */
@media (--motionOK) {
  .sun {
    transition: transform .5s var(--ease-elastic-3);
  }
}

للحصول على إيقاعات فريدة وسهلة الاستخدام في CSS، يمكنك استيراد جزء تخفيف التغييرات من Open Props:

@import "https://unpkg.com/open-props/easings.min.css";

/* usage example */
.sun {
  transition: transform .5s var(--ease-elastic-3);
}

الشمس

ستكون تغيّرات الشمس مرحة أكثر من القمر، ما سيحقق هذا التأثير من خلال الإرخاء السريع. من المفترض أن ترتد أشعة الشمس بمقدار صغير أثناء تدويرها، ويجب أن يهتز مركز الشمس بمقدار ضئيل عندما يتدرج.

تحدِّد الأنماط التلقائية (مظهر فاتح) الانتقالات، بينما تحدِّد أنماط المظهر الداكن عمليات التخصيص للانتقال إلى المظهر الفاتح:

​​.sun-and-moon {
  @media (--motionOK) {
    & > .sun {
      transition: transform .5s var(--ease-elastic-3);
    }

    & > .sun-beams {
      transition: 
        transform .5s var(--ease-elastic-4),
        opacity .5s var(--ease-3)
      ;
    }

    @nest [data-theme="dark"] & {
      & > .sun {
        transform: scale(1.75);
        transition-timing-function: var(--ease-3);
        transition-duration: .25s;
      }

      & > .sun-beams {
        transform: rotateZ(-25deg);
        transition-duration: .15s;
      }
    }
  }
}

في لوحة الرسوم المتحركة في "أدوات مطوري البرامج في Chrome"، يمكنك العثور على مخطط زمني لعمليات نقل الصور المتحركة. يمكن فحص مدة إجمالي الرسوم المتحركة والعناصر وتوقيت تغيير السرعة.

الانتقال من المظهر الفاتح إلى الداكن
الانتقال من المظهر الداكن إلى الفاتح

القمر

سبق أن تم إعداد ضوء القمر ووضعي المعتمة، ويمكنك إضافة أنماط انتقالية داخل الاستعلام عن الوسائط --motionOK لإضفاء الحيوية على المحتوى مع احترام الإعدادات المفضّلة لحركة المستخدم.

ويعد التوقيت مع التأخير والمدة أمرًا بالغ الأهمية في جعل هذا الانتقال نظيفًا. إذا اختفت الشمس في وقت مبكر جدًا، على سبيل المثال، لا يبدو الانتقال منظمًا أو مرحًا، بل يبدو فوضويًا.

​​.sun-and-moon {
  @media (--motionOK) {
    & .moon > circle {
      transform: translateX(-7px);
      transition: transform .25s var(--ease-out-5);

      @supports (cx: 1) {
        transform: translateX(0);
        cx: 17;
        transition: cx .25s var(--ease-out-5);
      }
    }

    @nest [data-theme="dark"] & {
      & > .moon > circle {
        transition-delay: .25s;
        transition-duration: .5s;
      }
    }
  }
}
الانتقال من المظهر الفاتح إلى الداكن
الانتقال من المظهر الداكن إلى الفاتح

يفضل الحركة المنخفضة

في معظم تحديات واجهة المستخدم الرسومية، أحاول الاحتفاظ ببعض الرسوم المتحركة، مثل التلاشي المتقاطع للتعتيم، للمستخدمين الذين يفضلون الحركة المنخفضة. ومع ذلك، كان هذا المكون أفضل مع تغييرات الحالة الفورية.

JavaScript

يتطلّب هذا المكوِّن مجهودًا كبيرًا على JavaScript، بدءًا من إدارة معلومات ARIA لقارئات الشاشة إلى الحصول على القيم وضبطها من وحدة التخزين المحلية.

تجربة تحميل الصفحة

كان من المهم عدم حدوث أي وميض ألوان عند تحميل الصفحة. إذا أشار المستخدم الذي يستخدم نظام ألوان داكنة إلى أنه يفضل الإضاءة باستخدام هذا المكون، ثم أعاد تحميل الصفحة، فستكون الصفحة داكنة في البداية، ثم تومض إلى فاتحة. ويؤدي منع ذلك إلى تشغيل مقدار صغير من حظر JavaScript باستخدام الهدف لضبط سمة HTML data-theme في أقرب وقت ممكن.

<script src="./theme-toggle.js"></script>

لتنفيذ ذلك، يتم أولاً تحميل علامة <script> عادية في المستند <head> قبل أيّ ترميز CSS أو <body>. عندما يصادف المتصفح نصًا برمجيًا غير مميّز مثل هذا، فإنه يشغّل التعليمة البرمجية وينفّذها قبل بقية نص HTML. عند استخدام لحظة الحظر هذه باعتدال، من الممكن ضبط سمة HTML قبل أن ترسم خدمة CSS الصفحة الصفحة، وبالتالي يتم منع الفلاش أو الألوان.

تتحقّق لغة JavaScript أولاً من الإعدادات المفضّلة للمستخدم في مساحة التخزين المحلية، وترجع إلى تفضيل النظام في حال عدم العثور على أي جزء في مساحة التخزين:

const storageKey = 'theme-preference'

const getColorPreference = () => {
  if (localStorage.getItem(storageKey))
    return localStorage.getItem(storageKey)
  else
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light'
}

يتم تحليل دالة لضبط تفضيل المستخدم في مساحة التخزين المحلية بعد ذلك:

const setPreference = () => {
  localStorage.setItem(storageKey, theme.value)
  reflectPreference()
}

تليها دالة لتعديل المستند بالإعدادات المفضّلة

const reflectPreference = () => {
  document.firstElementChild
    .setAttribute('data-theme', theme.value)

  document
    .querySelector('#theme-toggle')
    ?.setAttribute('aria-label', theme.value)
}

هناك شيء مهم يجب ملاحظته في هذه المرحلة وهو حالة تحليل وثيقة HTML. لا يتعرّف المتصفّح على الزر " #theme-toggle" بعد، لأنّ علامة <head> لم يتم تحليلها بالكامل. مع ذلك، يتضمّن المتصفّح علامة document.firstElementChild، تُعرف باسم علامة <html>. تحاول الدالة تعيين كليهما للحفاظ على مزامنتهما، ولكن عند التشغيل لأول مرة لن تكون قادرة إلا على تعيين علامة HTML. في البداية، لن يعثر querySelector على أي شيء في البداية ويضمن عامل تشغيل التسلسل الاختياري عدم وجود أخطاء في البنية في حال عدم العثور عليها وعند محاولة استدعاء دالة setAttribute.

بعد ذلك، يتم استدعاء الدالة reflectPreference() على الفور بحيث يتم ضبط سمة data-theme في مستند HTML:

reflectPreference()

لا يزال الزر بحاجة إلى السمة، لذا انتظِر حتى اكتمال حدث تحميل الصفحة، وسيكون من الآمن إجراء طلبات بحث وإضافة أدوات معالجة وضبط السمات على:

window.onload = () => {
  // set on load so screen readers can get the latest value on the button
  reflectPreference()

  // now this script can find and listen for clicks on the control
  document
    .querySelector('#theme-toggle')
    .addEventListener('click', onClick)
}

تجربة التبديل

عند النقر على الزر، يجب تبديل المظهر، في ذاكرة JavaScript وفي المستند. يجب فحص قيمة الموضوع الحالي واتخاذ قرار بشأن حالتها الجديدة. بمجرد تعيين الحالة الجديدة، احفظها وحدث المستند:

const onClick = () => {
  theme.value = theme.value === 'light'
    ? 'dark'
    : 'light'

  setPreference()
}

المزامنة مع النظام

ويتفرد مفتاح تبديل المظهر هذا بمزامنة الإعدادات المفضّلة للنظام أثناء تغيّرها. فإذا غيّر المستخدم الإعدادات المفضّلة للنظام بينما تظهر الصفحة وهذا المكوِّن، سيتغير مفتاح تبديل المظهر ليلائم تفضيل المستخدم الجديد، كما لو كان المستخدم قد تفاعل مع مفتاح التبديل في الوقت نفسه الذي حدث فيه تبديل النظام.

يمكنك تحقيق ذلك باستخدام JavaScript وحدث matchMedia للاستماع إلى التغييرات في الاستعلام عن الوسائط:

window
  .matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', ({matches:isDark}) => {
    theme.value = isDark ? 'dark' : 'light'
    setPreference()
  })
يؤدي تغيير الإعدادات المفضّلة لنظام MacOS إلى تغيير حالة تبديل المظهر.

الخلاصة

الآن بعد أن عرفت كيف فعلت ذلك، كيف يمكنك‽ 🙂

دعونا ننويع أساليبنا ونتعلم جميع طرق الإنشاء على الويب. يمكنك إنشاء عرض توضيحي وروابط تغريدات لي وسنضيفها إلى قسم الريمكسات في المنتدى أدناه.

ريمكسات من المنتدى