בניית רכיב של סרגל טעינה

סקירה כללית בסיסית על האופן שבו אפשר ליצור סרגל טעינה נגיש שמותאם לצבעים באמצעות הרכיב <progress>.

בפוסט הזה אני רוצה לשתף אתכם ברעיונות ליצירת סרגל טעינה נגיש שמתאים את הצבעים שלו, באמצעות הרכיב <progress>. נסו את ההדגמה והציגו את המקור.

הדגמה של מצב בהיר וכהה, לא מוגדר, הולך וגדל והשלמה ב-Chrome.

אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:

סקירה כללית

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

באתגר ה-GUI הזה השתמשנו ברכיב ה-HTML הקיים <progress> כדי לחסוך מאמץ בהנגשה. הצבעים והפריסות מרחיבים את גבולות ההתאמה האישית של הרכיב המובנה, כדי לשדרג את הרכיב ולהתאים אותו טוב יותר למערכות העיצוב.

כרטיסיות בהירות וכהות בכל דפדפן, שמציגות סקירה כללית של הסמל הניתן להתאמה מלמעלה למטה: Safari,‏ Firefox,‏ Chrome.
הדגמה ב-Firefox, ב-Safari, ב-Safari ל-iOS, ב-Chrome וב-Chrome ל-Android בערכות נושא בהירות וכהות.

Markup

בחרתי לעטוף את הרכיב <progress> ב-<label> כדי שאוכל לדלג על מאפייני הקשר המפורשים ולהשתמש בקשר משתמע. בנוסף, תייגתי רכיב הורה שמושפע ממצב הטעינה, כדי שטכנולוגיות של קוראי מסך יוכלו להעביר את המידע הזה למשתמש.

<progress></progress>

אם אין value, ההתקדמות של הרכיב לא מוגדרת. ערך ברירת המחדל של המאפיין max הוא 1, כך שההתקדמות היא בין 0 ל-1. לדוגמה, אם מגדירים את max ל-100, הטווח יהיה 0-100. בחרתי להישאר בטווח שבין 0 ל-1, ולתרגם את ערכי ההתקדמות ל-0.5 או 50%.

התקדמות עם תווית

ביחס משתמע, רכיב התקדמות מוקף בתווית, כך:

<label>Loading progress<progress></progress></label>

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

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

עם ה-CSS הנלווה הבא מ-WebAIM:

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

צילום מסך של כלי הפיתוח שבו מוצג הרכיב שנגיש רק לקורא מסך.

האזור שמושפע מהתקדמות הטעינה

אם הראייה שלכם תקינה, קל לשייך את מחוון ההתקדמות לאלמנטים ולאזורים קשורים בדף, אבל למשתמשים עם לקות ראייה זה לא כל כך ברור. כדי לשפר את המצב, אפשר להקצות את המאפיין aria-busy לרכיב העליון ביותר שישתנה כשהטעינה תסתיים. בנוסף, מציינים את הקשר בין ההתקדמות לבין אזור הטעינה באמצעות aria-describedby.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

ב-JavaScript, מחליפים את aria-busy ל-true בתחילת המשימה, ול-false בסיום המשימה.

תוספות למאפייני Aria

למרות שהתפקיד המשתמע של רכיב <progress> הוא progressbar, הגדרתי אותו באופן מפורש בשביל דפדפנים שאין להם את התפקיד המשתמע הזה. הוספתי גם את המאפיין indeterminate כדי להעביר את הרכיב באופן מפורש למצב לא ידוע, שברור יותר מאשר לראות שלרכיב לא מוגדר value.

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

משתמשים ב-tabindex="-1" כדי לאפשר התמקדות ברכיב ההתקדמות מ-JavaScript. זה חשוב לטכנולוגיית קורא המסך, כי אם נותנים להתקדמות את המיקוד כשההתקדמות משתנה, המשתמש יקבל הודעה על מידת ההתקדמות המעודכנת.

סגנונות

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

פריסה

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

פריסה של <progress>

הרוחב של רכיב ההתקדמות לא משתנה, כדי שהוא יוכל להתכווץ ולהתרחב בהתאם למקום שנדרש בעיצוב. הסגנונות המובנים מוסרים על ידי הגדרת appearance ו-border ל-none. הסיבה לכך היא שכל דפדפן משתמש בסגנונות משלו לרכיב, וצריך לנרמל את הרכיב בכל הדפדפנים.

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

הערך של 1e3px בשביל _radius משתמש בכתיב מדעי כדי לבטא מספר גדול, ולכן הערך של border-radius תמיד מעוגל. הפרמטר הזה מקביל ל-1000px. אני אוהב להשתמש בזה כי המטרה שלי היא להשתמש בערך גדול מספיק כדי שאוכל להגדיר אותו ולשכוח ממנו (וקצר יותר לכתוב אותו מאשר 1000px). קל גם להגדיל אותו עוד יותר אם צריך: פשוט משנים את ה-3 ל-4, ואז 1e4px שווה ערך ל-10000px.

הסגנון overflow: hidden נמצא בשימוש והוא שנוי במחלוקת. זה הקל על כמה דברים, כמו לא להעביר ערכי border-radius למסלול ולרכיבי המילוי של המסלול, אבל זה גם אומר שאף אחד מהצאצאים של ההתקדמות לא יכול להיות מחוץ לרכיב. אפשר ליצור גרסה נוספת של רכיב ההתקדמות המותאם אישית הזה בלי overflow: hidden, וזה עשוי לפתוח כמה הזדמנויות לאנימציות או למצבי השלמה טובים יותר.

ההתקדמות הושלמה

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

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

צילום מסך של סרגל הטעינה ב-100% עם סימן וי בסוף.

צבע

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

סגנונות בהירים וכהים לדפדפן

כדי להוסיף לאתר רכיב <progress> שמותאם למצב כהה ובהיר, צריך רק להוסיף את הקוד color-scheme.

progress {
  color-scheme: light dark;
}

צבע המילוי של ההתקדמות בנכס יחיד

כדי להוסיף גוון לרכיב <progress>, משתמשים ב-accent-color.

progress {
  accent-color: rebeccapurple;
}

שימו לב שצבע הרקע של המסלול משתנה מבהיר לכהה בהתאם לaccent-color. הדפדפן מוודא שהניגודיות תקינה: די מגניב.

צבעים בהירים וכהים בהתאמה אישית מלאה

מגדירים שני מאפיינים מותאמים אישית ברכיב <progress>, אחד לצבע של הפס והשני לצבע של ההתקדמות בפס. בתוך שאילתת המדיה prefers-color-scheme, מספקים ערכי צבע חדשים לרצועה ולהתקדמות הרצועה.

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

סגנונות מיקוד

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

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

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

סגנונות אישיים בדפדפנים שונים

כדי להתאים אישית את הסגנונות, בוחרים את החלקים של רכיב <progress> שכל דפדפן חושף. השימוש ברכיב ההתקדמות הוא תג יחיד, אבל הוא מורכב מכמה רכיבי צאצא שנחשפים באמצעות סלקטורים מדומים של CSS. כלי הפיתוח ל-Chrome יציגו את הרכיבים האלה אם תפעילו את ההגדרה:

  1. לוחצים לחיצה ימנית על הדף ובוחרים באפשרות בדיקת המרכיב כדי לפתוח את כלי הפיתוח.
  2. לוחצים על גלגל השיניים של ההגדרות בפינה השמאלית העליונה של החלון של כלי הפיתוח.
  3. בקטע Elements, מוצאים את תיבת הסימון Show user agent shadow DOM ומפעילים אותה.

צילום מסך שמראה איפה בכלי הפיתוח אפשר להפעיל את החשיפה של ה-shadow DOM של סוכן המשתמש.

סגנונות של Safari ו-Chromium

דפדפנים שמבוססים על WebKit, כמו Safari ו-Chromium, חושפים את ::-webkit-progress-bar ו-::-webkit-progress-value, שמאפשרים להשתמש בקבוצת משנה של CSS. בינתיים, מגדירים את background-color באמצעות המאפיינים המותאמים אישית שנוצרו קודם, שמתאימים למצב בהיר ולמצב כהה.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

צילום מסך שמראה את הרכיבים הפנימיים של רכיב ההתקדמות.

סגנונות של Firefox

‫Firefox חושף רק את הסלקטור המדומה ::-moz-progress-bar ברכיב <progress>. זה גם אומר שאנחנו לא יכולים לשנות את הצבע של המסלול ישירות.

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

צילום מסך של Firefox והמיקום של חלקי רכיב ההתקדמות.

צילום מסך של פינת ניפוי הבאגים שבה מוצג סרגל הטעינה פועל ב-Safari, ב-Safari ל-iOS, ב-Firefox, ב-Chrome וב-Chrome ל-Android.

שימו לב שב-Firefox צבע הפס מוגדר מ-accent-color, וב-Safari ב-iOS הפס בצבע כחול בהיר. אותו דבר קורה במצב כהה: ל-Firefox יש מסלול כהה אבל לא את הצבע המותאם אישית שהגדרנו, והוא פועל בדפדפנים שמבוססים על Webkit.

Animation

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

אנימציה של מילוי הטראק

הוספת מעבר אל inline-size של רכיב ההתקדמות פועלת ב-Chromium אבל לא ב-Safari. בנוסף, ב-Firefox לא נעשה שימוש במאפיין מעבר ב-::-moz-progress-bar.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

הנפשת המצב :indeterminate

כאן אני קצת יותר יצירתי, כדי שאוכל לספק אנימציה. נוצר רכיב פסאודו ל-Chromium ומוחל שיפוע שמונפש קדימה ואחורה בכל שלושת הדפדפנים.

המאפיינים המותאמים אישית

מאפיינים מותאמים אישית הם נהדרים להרבה דברים, אבל אחד הדברים האהובים עליי הוא פשוט לתת שם לערך CSS שנראה קסום. הנה linear-gradient די מורכבת, אבל עם שם נחמד. המטרה ותרחישי השימוש שלו ברורים.

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

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

הקיפריימים

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

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

טירגוט לכל דפדפן

לא כל דפדפן מאפשר ליצור רכיבי פסאודו ברכיב <progress> עצמו או להוסיף אנימציה לסרגל ההתקדמות. יותר דפדפנים תומכים בהנפשה של הרצועה מאשר ברכיב פסאודו, אז אני משדרג מרכיבי פסאודו כבסיס לפסי הנפשה.

רכיב פסאודו של Chromium

‫Chromium מאפשר להשתמש ברכיב המדומה ::after עם מיקום כדי לכסות את הרכיב. המאפיינים המותאמים אישית הלא מוגדרים נמצאים בשימוש, והאנימציה של התנועה קדימה ואחורה פועלת היטב.

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
סרגל ההתקדמות ב-Safari

ב-Safari, המאפיינים המותאמים אישית והאנימציה מוחלים על סרגל ההתקדמות של הרכיב המדומה:

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
סרגל ההתקדמות של Firefox

ב-Firefox, המאפיינים המותאמים אישית והאנימציה חלים גם על סרגל ההתקדמות של הרכיב המדומה:

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

ל-JavaScript יש תפקיד חשוב ברכיב <progress>. הוא שולט בערך שנשלח לרכיב ומוודא שיש מספיק מידע במסמך בשביל קוראי מסך.

const state = {
  val: null
}

בהדגמה יש כפתורים לשליטה בהתקדמות. הם מעדכנים את state.val ואז קוראים לפונקציה לעדכון ה-DOM.

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

בפונקציה הזו מתרחש התיאום של ממשק המשתמש וחוויית המשתמש. כדי להתחיל, יוצרים פונקציית setProgress(). לא נדרשים פרמטרים כי יש לו גישה לאובייקט state, לרכיב ההתקדמות ולאזור <main>.

const setProgress = () => {
  
}

הגדרת סטטוס הטעינה באזור <main>

בהתאם למצב ההתקדמות, צריך לעדכן את המאפיין aria-busy של הרכיב <main>:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

מחיקת המאפיינים אם סכום הטעינה לא ידוע

אם הערך לא ידוע או לא מוגדר, null בשימוש הזה, צריך להסיר את המאפיינים value ו-aria-valuenow. הפעולה הזו תהפוך את <progress> ללא מוגדר.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

פתרון בעיות בחישובים עשרוניים ב-JavaScript

מכיוון שבחרתי להישאר עם ערך ברירת המחדל המקסימלי של ההתקדמות, שהוא 1, פונקציות ההגדלה וההפחתה של ההדגמה משתמשות במתמטיקה עשרונית. JavaScript ושפות אחרות לא תמיד טובות בזה. הנה פונקציית roundDecimals() שתחתוך את החלק המיותר מהתוצאה המתמטית:

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

מעגלים את הערך כדי שיהיה אפשר להציג אותו בצורה קריאה:

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

הגדרת ערך לקוראי מסך ולמצב הדפדפן

הערך משמש בשלושה מיקומים ב-DOM:

  1. המאפיין value של הרכיב <progress>.
  2. המאפיין aria-valuenow.
  3. תוכן הטקסט הפנימי של <progress>.
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

התמקדות בהתקדמות

אחרי שהערכים מתעדכנים, משתמשים רואים את השינוי בהתקדמות, אבל משתמשים בקורא מסך עדיין לא מקבלים הודעה על השינוי. ממקדים את הרכיב <progress> והדפדפן יכריז על העדכון.

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

צילום מסך של אפליקציית VoiceOver ב-Mac OS שמקריאה למשתמש את ההתקדמות של סרגל הטעינה.

סיכום

עכשיו, אחרי שאתה יודע איך אני עשיתי את זה, איך אתה היית עושה את זה? 🙂

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

בואו נגוון את הגישות שלנו ונלמד את כל הדרכים לבנות באינטרנט.

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

הרשאה לקהילה ליצור רמיקסים