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

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

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

הגדלנו את הגודל של הלחצן הדגמה כדי שיהיה קל לראות אותו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-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". במקרה של הלחצן הזה, הוא יכריז על 'light' (בהיר) או 'dark' (כהה), בהתאם למשמעות של 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 מופיעה ציר זמן של מעברי אנימציה. אפשר לבדוק את משך האנימציה הכולל, את הרכיבים ואת תזמון ההאטה.

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

הירח

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

סיכום

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

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

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