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

סקירה כללית בסיסית של בניית סרגל טעינה נגיש ומותאם לצבעים באמצעות האלמנט <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 בסיום.

הוספות של מאפייני 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 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;
}

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

תמונות המפתח

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

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

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

צור הדגמה (דמו), ציוץ לי קישורים ואני אוסיף אותה לקטע 'רמיקסים של הקהילה' למטה!

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