نظرة عامة أساسية حول كيفية إنشاء مكوّن تبديل متوافق وسهل الاستخدام للموضوعات
في هذه المشاركة، أريد مشاركة أفكار حول طريقة إنشاء مكوّن لتبديل المظهر الداكن والفاتح. جرِّب العرض التجريبي.
إذا كنت تفضّل مشاهدة الفيديوهات، يمكنك الاطّلاع على نسخة من هذه المشاركة على YouTube:
نظرة عامة
قد يقدّم موقع إلكتروني إعدادات للتحكّم في نظام الألوان بدلاً من الاعتماد بالكامل على الإعدادات المفضّلة للنظام. وهذا يعني أنّه يمكن للمستخدمين التصفّح في وضع غير الإعدادات المفضّلة للنظام. على سبيل المثال، يستخدم المستخدم شاشة برمجية بمظهر فاتح، ولكنّه يفضّل عرض الموقع الإلكتروني بشاشة برمجية ذات مظهر داكن.
هناك العديد من الاعتبارات المتعلقة بهندسة الويب عند إنشاء هذه الميزة. على سبيل المثال، يجب إبلاغ المتصفّح بالخيار المفضّل في أقرب وقت ممكن لمنع وميض ألوان الصفحة، ويجب مزامنة عنصر التحكّم أولاً مع النظام ثم السماح بالاستثناءات المخزّنة من جهة العميل.
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 داخله على الجوانب المرئية والمتحرّكة. في هذه المرحلة، يمكن أن يصبح الرمز الجميل حيويًا.
مظهر فاتح
لكي تحدث عمليات التحجيم والدوران في الصور المتحركة من مركز أشكال 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);
}
}
}
مظهر داكن
يجب أن تزيل أنماط القمر أشعة الشمس، وتكبِّر دائرة الشمس وتنقل قناع الدائرة.
.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()
})
الخاتمة
الآن بعد أن عرفت كيف فعلت ذلك، كيف ستفعل ذلك؟ 🙂
لننوّع أساليبنا ونتعرّف على جميع الطرق لإنشاء تطبيقات على الويب. أنشئ عرضًا توضيحيًا وأرسِل إلينا رابطًا على Twitter، وسنضيفه إلى قسم الريمكسات التي أنشأها المستخدمون أدناه.
الريمكسات التي أنشأها المستخدمون
- @NathanG على Codepen باستخدام Vue
- @ShadowShahriar على Codepen
- @tomayac كـ عنصر مخصّص
- @bramus باستخدام JavaScript العادي
- @JoshWComeau باستخدام التفاعل