יצירת רכיב להחלפת עיצוב

סקירה כללית בסיסית על בניית רכיב מתג נושאים מותאם ונגיש.

בפוסט הזה אני רוצה לשתף את הדרך שבה פיתחנו רכיב להעברה בין עיצוב כהה לעיצוב בהיר. לנסות את הדמו

הדגמה כדי שיהיה קל לראות אותו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-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.

הסמל של Scalable Vector Graphics ‏ (SVG)

SVG מאפשר ליצור צורות באיכות גבוהה שניתן להתאים אותן למגוון גדלים, עם סימון מינימלי. אינטראקציה עם הלחצן יכולה להפעיל מצבים חזותיים חדשים של הווקטורים, ולכן SVG מתאים במיוחד לסמלים.

ה-markup הבא של 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>
גרפיקה עם שלוש שכבות אנכיות שמראה איך עובדת יצירת המסכה. השכבה העליונה היא ריבוע לבן עם עיגול שחור. השכבה האמצעית היא סמל השמש.
השכבה התחתונה מסומנת בתווית &#39;תוצאה&#39;, ומוצג בה סמל השמש עם חור במקום שבו נמצא העיגול השחור בשכבה העליונה.

מסכות ב-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

בשלב הזה, הלחצן צריך להיות פונקציונלי ועם מצב, אבל ללא מעברים. בקטעים הבאים נסביר איך מגדירים איך ואילו מעברים.

שיתוף של שאילתות מדיה וייבוא של תנועות איטיות

כדי שתוכלו להשתמש בקלות במעברים ובאנימציות בהתאם להעדפות התנועה של המשתמש במערכת ההפעלה, התוסף Custom Media של PostCSS מאפשר להשתמש בתחביר של טיוטת התקן של CSS למשתני שאילתה של מדיה:

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

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

כדי להשתמש באפשרויות ייחודיות וקלות לשימוש של CSS easings, אפשר לייבא את הקטע 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;
      }
    }
  }
}

בחלונית Animation בכלים למפתחים של Chrome מופיעה ציר זמן של מעברי אנימציה. אפשר לבדוק את משך האנימציה הכולל, את הרכיבים ואת תזמון ההאטה.

מעבר מבהיר לכהה
מעבר מכהה לבהיר

הירח

המיקומים של הירח בהיר וחשוך כבר מוגדרים. כדי להוסיף תנועה לתמונה תוך שמירה על העדפות התנועה של המשתמש, מוסיפים סגנונות מעבר בתוך שאילתה ה-Media --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 משנה את מצב המתג של העיצוב

סיכום

עכשיו, אחרי שסיפרתי לך איך עשיתי את זה, איך היית עושה את זה? 🙂

נרחיב את הגישות שלנו ונלמד את כל הדרכים לפיתוח באינטרנט. אתם יכולים ליצור גרסת דמו, לשלוח לי קישורים בטוויטר ואוסיף אותה לקטע 'רמיקסים של הקהילה' שבהמשך.

רמיקסים של הקהילה