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

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

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

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

إذا كنت تفضّل مشاهدة فيديو، إليك نسخة من هذا المنشور على 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، يتم ضبط stroke لكل سطر. تكوّن الخطوط وأشكال الدوائر شمسًا جميلة مع أشعة.

القمر

لإنشاء وهم الانتقال السلس بين الضوء (الشمس) والظلام (القمر)، يتم استخدام القمر كتحسين لرمز الشمس، وذلك باستخدام قناع 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: 1px) {
        transform: translateX(0);
        cx: 17px;
      }
    }
  }
}

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

Animation

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

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

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

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

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

لاستخدام دوال التباطؤ والتسارع الفريدة والسهلة، استورِد جزء easings من 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;
      }
    }
  }
}

في لوحة الصور المتحركة ضمن &quot;أدوات مطوّري البرامج في Chrome&quot;، يمكنك العثور على مخطط زمني لعمليات انتقال الصور المتحركة. يمكن فحص مدة الحركة الإجمالية والعناصر وتوقيت التباطؤ والتسارع.

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

القمر

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

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

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

      @supports (cx: 1px) {
        transform: translateX(0);
        cx: 17px;
        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 إلى تغيير حالة مفتاح تبديل المظهر

الخاتمة

بعد أن عرفت كيف فعلتُ ذلك، كيف ستفعل أنت ذلك؟ 🙂

لنستكشف الطرق المختلفة لإنشاء مواقع إلكترونية على الويب. يمكنك إنشاء عرض توضيحي، إرسال تغريدة إليّ تتضمّن رابطًا إليه، وسأضيفه إلى قسم "ريمكسات من المنتدى" أدناه.

ريمكسات من إنشاء المنتدى