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

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

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

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

إذا كنت تفضّل مشاهدة الفيديوهات، يمكنك الاطّلاع على نسخة من هذه المشاركة على 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 المطلوبة في العنصر، أضِف السمتَين height وwidth للأسباب نفسها التي تتطلّب تضمين سمات المقاييس للصور.

الشمس

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

يتألّف الرسم الشمسي من دائرة وخطوط تتوفّر لها أشكال 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 للتأكّد من أنّ النتيجة ليست كبيرة جدًا أو تسبب مشاكل في التنسيق. توفّر سمتا height وwidth المضمّنتَين في ملف SVG بالإضافة إلى استخدام currentColor الحدّ الأدنى من قواعد الأنماط التي يمكن للمتصفّح استخدامها في حال عدم تحميل ملف CSS. ويؤدي ذلك إلى توفير أساليب دفاعية جيدة ضد الاضطرابات في الشبكة.

التنسيق

يشغل مكوّن تبديل المظهر مساحة سطح صغيرة، لذا لا تحتاج إلى شبكة أو صندوق مرن لتنسيق المحتوى. بدلاً من ذلك، يتم استخدام مواضع 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);
  }
}

للحصول على تأثيرات CSS الفريدة والسهلة الاستخدام، استورِد الجزء 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;
      }
    }
  }
}

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

انتقال من الإضاءة الساطعة إلى الإضاءة الخافتة
انتقال من الإضاءة الخافتة إلى الإضاءة الساطعة

القمر

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

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

من المهم عدم حدوث وميض للألوان عند تحميل الصفحة. إذا كان أحد المستخدمين يستخدم ملف شخصي يعتمد على skemة ألوان قاتمة ويشير إلى أنّه يفضّل استخدام الألوان الفاتحة مع هذا المكوّن، ثمّ تتم إعادة تحميل الصفحة، ستظهر الصفحة في البداية قاتمة ثمّ ستظهر فاتحة. لمنع حدوث ذلك، كان علينا تشغيل قدر صغير من 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> بالكامل. ومع ذلك، يحتوي browser على document.firstElementChild ، والمعروفة أيضًا باسم علامة <html>. تحاول الدالة ضبط كليهما للحفاظ على مزامنتهما، ولكن في التشغيل الأول، لن تتمكّن من ضبط علامة HTML إلا. لن يعثر الرمز المبرمَج querySelector على أي عنصر في البداية، ويضمن عامل السلسلة الاختيارية عدم حدوث أخطاء نحوية عند عدم العثور عليه ومحاولة استدعاء الدالة setAttribute.

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

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 إلى تغيير حالة تبديل المظهر

الخاتمة

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

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

الريمكسات التي أنشأها المستخدمون