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

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

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

تم زيادة حجم زر العرض التوضيحي لتسهيل رؤيته

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

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