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

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

צילום מסך של devtools שמציג את הרכיב &#39;מוכן להצגה במסך&#39; בלבד.

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

אם יש לכם ראייה תקינה, יכול להיות שיהיה לכם קל לשייך אינדיקטור של התקדמות לרכיבים ולאזורים קשורים בדף, אבל למשתמשים עם לקות ראייה זה לא כל כך ברור. כדי לשפר את המצב, אפשר להקצות את המאפיין 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 DevTools:

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

צילום מסך של המקום ב-DevTools שבו מפעילים את חשיפת ה-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;
}

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

תמונות המפתח

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

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()
}

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

סיכום

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

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

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

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

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