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

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

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

חשוך בהיר, קבוע, במגמת עלייה ומושלם בהדגמה (דמו) ב-Chrome.

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

סקירה

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

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

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

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 בסיום המשימה.

תוספות של מאפיין אריה

התפקיד המשתמע של <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 ומפעילים אותה.

צילום מסך שמראה איפה בכלי הפיתוח זמינים חשיפת ה-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, iOS Safari, Firefox, Chrome ו-Chrome ב-Android.

שימו לב שב-Firefox, צבע הטראק מוגדר כ-accent-color, וב-iOS Safari מופיע מסלול בצבע תכלת. אותו הדבר במצב כהה: ב-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>

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

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>. כדאי להתנסות!

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

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

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