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