Eine Ladebalkenkomponente erstellen

Eine grundlegende Übersicht zum Erstellen eines adaptiven und barrierefreien Ladebalkens mit dem <progress>-Element.

In diesem Beitrag möchte ich darüber sprechen, wie sich mit dem <progress>-Element eine adaptive und zugängliche Ladeleiste für die Farbe erstellen lässt. Probieren Sie die Demo aus und sehen Sie sich den Quellcode an.

In Chrome wurden die hellen und dunklen, unbestimmten, zunehmenden und abgeschlossenen Elemente vorgestellt.

Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:

Überblick

Das Element <progress> liefert dem Nutzer visuelles und akustisches Feedback zur Fertigstellung. Dieses visuelle Feedback ist nützlich für Szenarien wie den Fortschritt in einem Formular, das Anzeigen von Download- oder Upload-Informationen oder sogar die Anzeige, dass der Fortschritt unbekannt ist, die Arbeit aber noch aktiv ist.

Bei dieser GUI-Herausforderung wurde das vorhandene HTML-<progress>-Element verwendet, um die Zugänglichkeit etwas zu verringern. Mit den Farben und Layouts werden die Grenzen der Anpassung für das integrierte Element erweitert, um die Komponente zu modernisieren und dafür zu sorgen, dass sie besser in Designsysteme passt.

Helle und dunkle Tabs in jedem Browser, die einen Überblick über das adaptive Symbol von oben nach unten bieten: Safari, Firefox und Chrome.
Demo wird in Firefox, Safari, iOS Safari, Chrome und Android Chrome in hellen und dunklen Designs gezeigt.

Markup

Ich habe mich dafür entschieden, das <progress>-Element in eine <label> einzubinden, damit ich die expliziten Beziehungsattribute zugunsten einer impliziten Beziehung überspringen kann. Ich habe auch ein übergeordnetes Element gekennzeichnet, das vom Ladestatus betroffen ist, sodass Screenreader-Technologien diese Informationen an den Nutzer weitergeben können.

<progress></progress>

Wenn value nicht vorhanden ist, ist der Fortschritt des Elements unbestimmt. Der Standardwert für das max-Attribut ist 1, der Fortschritt liegt also zwischen 0 und 1. Wenn Sie beispielsweise max auf 100 setzen, wird der Bereich auf 0–100 gesetzt. Ich habe die Grenzwerte 0 und 1 durchgesetzt und die Werte auf 0, 5 oder 50 % gesetzt.

Fortschritt bei Labelumbruch

In einer impliziten Beziehung wird ein Fortschrittselement wie folgt von einem Label umschlossen:

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

In meiner Demo habe ich mich dafür entschieden, das Label nur für Screenreader zu verwenden. Dazu setzen Sie den Labeltext in ein <span> und wenden einige Stile darauf an, sodass er außerhalb des sichtbaren Bildschirmbereichs liegt:

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

Mit dem folgenden CSS von 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;
}

Screenshot der Entwicklertools mit dem Bildschirmhelfer-Element

Vom Ladefortschritt betroffener Bereich

Wenn Sie ein gesundes Sehvermögen haben, kann es einfach sein, zusammenhängenden Elementen und Seitenbereichen eine Fortschrittsanzeige zuzuordnen. Für sehbehinderte Nutzer ist dies jedoch nicht so klar. Sie können dies verbessern, indem Sie das Attribut aria-busy dem obersten Element zuweisen, das sich nach Abschluss des Ladevorgangs ändert. Geben Sie außerdem mit aria-describedby eine Beziehung zwischen dem Fortschritt und der Ladezone an.

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

Ändern Sie in JavaScript zu Beginn der Aufgabe aria-busy in true und nach Abschluss der Aufgabe zu false.

Ergänzungen für Aria-Attribute

Während die implizite Rolle eines <progress>-Elements progressbar ist, habe ich sie für Browser ohne diese implizite Rolle explizit festgelegt. Außerdem habe ich das Attribut indeterminate hinzugefügt, um das Element explizit in den Status „Unbekannt“ zu versetzen. Dies ist klarer als die Beobachtung, dass für das Element kein value festgelegt ist.

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

Verwenden Sie tabindex="-1", damit das Fortschrittselement über JavaScript fokussiert werden kann. Dies ist für die Screenreader-Technologie wichtig, da der Fokus beim Ändern des Fortschritts dem Nutzer mitgeteilt wird, wie weit der aktualisierte Fortschritt erreicht ist.

Stile

Das Progress-Element ist etwas schwierig bei der Gestaltung. Integrierte HTML-Elemente haben spezielle verborgene Teile, die schwer auszuwählen sind und häufig nur eine begrenzte Anzahl von Eigenschaften haben, die festgelegt werden können.

Layout

Die Layoutstile sollen eine gewisse Flexibilität in Bezug auf Größe und Labelposition des Fortschrittselements ermöglichen. Es wird ein spezieller Abschlussstatus hinzugefügt, der ein nützlicher, aber nicht erforderlicher zusätzlicher visueller Hinweis sein kann.

<progress>-Layout

Die Breite des Fortschrittselements bleibt unberührt, sodass es mit dem im Design erforderlichen Platz schrumpfen und größer werden kann. Die integrierten Stile werden entfernt, wenn Sie appearance und border auf none setzen. So kann das Element browserübergreifend normalisiert werden, da jeder Browser seinen eigenen Stil für sein Element hat.

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;
}

Der Wert von 1e3px für _radius verwendet die wissenschaftliche Zahlennotation, um eine große Zahl auszudrücken, sodass border-radius immer gerundet wird. Er entspricht 1000px. Ich verwende diese Methode, weil mein Ziel ist, einen Wert zu verwenden, der groß genug ist, dass ich ihn festlegen und vergessen kann (und er ist kürzer als 1000px). Sie können ihn bei Bedarf auch noch vergrößern: Ändern Sie einfach die 3 in eine 4, dann entspricht 1e4px 10000px.

overflow: hidden wird verwendet und war umständlich. Es machte einige Dinge einfach, z. B. musste border-radius-Werte nicht an den Track übergeben und Füllelemente nicht verfolgen. Es bedeutete aber auch, dass keine untergeordneten Elemente des Fortschritts außerhalb des Elements leben konnten. Eine weitere Iteration dieses benutzerdefinierten Fortschrittselements könnte ohne overflow: hidden ausgeführt werden und eröffnet möglicherweise einige Möglichkeiten für Animationen oder bessere Abschlussstatus.

Vorgang abgeschlossen

CSS-Selektoren erledigen hier die schwierige Arbeit, indem sie das Maximum mit dem Wert vergleichen. Wenn sie übereinstimmen, ist der Fortschritt abgeschlossen. Wenn der Vorgang abgeschlossen ist, wird ein Pseudoelement generiert und an das Ende des Fortschrittselements angehängt, was einen schönen zusätzlichen visuellen Hinweis auf den Abschluss liefert.

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

Screenshot des Ladebalkens mit 100% mit einem Häkchen am Ende

Farbe

Der Browser verwendet eigene Farben für das Fortschrittselement und kann mit nur einer CSS-Eigenschaft hell und dunkel eingestellt werden. Darauf aufbauend können spezielle browserspezifische Selektoren sein.

Helle und dunkle Browserdesigns

Wenn für Ihre Website ein dunkles und helles adaptives <progress>-Element verwendet werden soll, benötigen Sie lediglich color-scheme.

progress {
  color-scheme: light dark;
}

Füllfarbe für Fortschritt einer einzelnen Property

Wenn Sie ein <progress>-Element einfärben möchten, verwenden Sie accent-color.

progress {
  accent-color: rebeccapurple;
}

Beachten Sie, dass die Hintergrundfarbe des Tracks je nach accent-color von hell zu dunkel wechselt. Der Browser sorgt für den richtigen Kontrast: ziemlich übersichtlich.

Vollständig individuelle helle und dunkle Farben

Legen Sie zwei benutzerdefinierte Eigenschaften für das <progress>-Element fest: eine für die Trackfarbe und eine für die Trackfortschrittsfarbe. Geben Sie in der Medienabfrage prefers-color-scheme neue Farbwerte für den Track an und verfolgen Sie den Fortschritt.

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%);
  }
}

Fokusstile

Zuvor haben wir dem Element einen negativen Tabindex zugewiesen, damit es programmatisch fokussiert werden kann. Verwenden Sie :focus-visible, um den Fokus anzupassen und den intelligenteren Fokusring-Stil zu aktivieren. Damit wird bei Mausklick und Fokus nicht der Fokusring angezeigt, bei Tastaturklicks jedoch schon. Im YouTube-Video wird dies ausführlich erklärt. Es lohnt sich, es noch einmal zu sehen.

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

Screenshot der Ladeleiste mit einem Fokusring darum Alle Farben sind identisch.

Benutzerdefinierte Designs für verschiedene Browser

Passen Sie die Stile an, indem Sie die Teile eines <progress>-Elements auswählen, die der jeweilige Browser verfügbar macht. Das progress-Element besteht aus einem einzelnen Tag, das jedoch aus einigen untergeordneten Elementen besteht, die über CSS-Pseudoselektoren verfügbar gemacht werden. Die Chrome-Entwicklertools zeigen diese Elemente an, wenn Sie die Einstellung aktivieren:

  1. Klicken Sie mit der rechten Maustaste auf Ihre Seite und wählen Sie Untersuchen aus, um die Entwicklertools aufzurufen.
  2. Klicke im Fenster „Entwicklertools“ oben rechts auf das Zahnradsymbol für die Einstellungen.
  3. Aktivieren Sie unter der Überschrift Elemente das Kästchen User-Agent-Shadow-DOM anzeigen.

Screenshot des Bereichs, in dem in den Entwicklertools die Offenlegung des User-Agent-Shadow-DOM aktiviert werden kann.

Safari- und Chromium-Stile

WebKit-basierte Browser wie Safari und Chromium stellen ::-webkit-progress-bar und ::-webkit-progress-value bereit, wodurch ein Teil von CSS verwendet werden kann. Legen Sie background-color vorerst mit den zuvor erstellten benutzerdefinierten Eigenschaften fest, die sich an helle und dunkle Eigenschaften anpassen.

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

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

Screenshot mit den inneren Elementen des progress-Elements

Firefox-Stile

Firefox stellt den Pseudoselektor ::-moz-progress-bar nur für das Element <progress> bereit. Das bedeutet auch, dass wir den Titel nicht direkt einfärben können.

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

Screenshot von Firefox und wo die Teile des Fortschrittselements zu finden sind

Screenshot der Debugging Corner, in dem die Ladeleiste in Safari, iOS Safari, Firefox, Chrome und Chrome unter Android angezeigt wird.

Bei Firefox wird die Farbe für Tracks ab accent-color verwendet, in iOS Safari dagegen ein hellblauer Track. Dasselbe gilt für den dunklen Modus: Firefox hat eine dunkle Spur, aber nicht die von uns festgelegte benutzerdefinierte Farbe, und es funktioniert in Webkit-basierten Browsern.

Animation

Bei der Arbeit mit in den Browser integrierten Pseudoselektoren ist dies häufig mit einer begrenzten Anzahl zulässiger CSS-Eigenschaften verbunden.

Animieren des Titels mit vollem Akku

Das Hinzufügen eines Übergangs zum inline-size des Fortschrittselements funktioniert in Chromium, aber nicht in Safari. Außerdem verwendet Firefox keine Übergangseigenschaft für ::-moz-progress-bar.

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

Status :indeterminate wird animiert

Hier bin ich etwas kreativer, damit ich eine Animation bereitstellen kann. Ein Pseudoelement für Chromium wird erstellt und ein Farbverlauf angewendet, der in allen drei Browsern hin und her animiert wird.

Benutzerdefinierte Eigenschaften

Benutzerdefinierte Eigenschaften eignen sich für viele Dinge, aber einer meiner Favoriten ist es, einem ansonsten magisch aussehenden CSS-Wert einfach einen Namen zu geben. Im Folgenden finden Sie eine ziemlich komplexe linear-gradient, aber mit einem schönen Namen. Zweck und Anwendungsfälle sind klar verständlich.

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;
}

Benutzerdefinierte Eigenschaften tragen auch dazu bei, dass der Code DRY bleibt, da wir diese browserspezifischen Selektoren nicht zusammen gruppieren können.

Die Keyframes

Das Ziel ist eine unendliche Animation, die hin und her fließt. Die Start- und End-Keyframes werden in CSS festgelegt. Für eine Animation ist nur der mittlere Keyframe bei 50% erforderlich. So kehrt er immer wieder zum Ausgangspunkt zurück.

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

Ausrichtung auf jeden Browser

Nicht jeder Browser erlaubt die Erstellung von Pseudoelementen im <progress>-Element selbst oder die Animation der Fortschrittsanzeige. Die Animation eines Tracks wird von mehr Browsern unterstützt als von einem Pseudoelement. Deshalb führe ich ein Upgrade von Pseudoelementen als Basis auf animierte Balken aus.

Chromium-Pseudoelement

Das Pseudoelement ::after mit einer Position, um das Element zu verdecken, ist in Chromium zulässig. Dabei werden die unbestimmten benutzerdefinierten Eigenschaften verwendet und die Animation hin und her funktioniert sehr gut.

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-Fortschrittsanzeige

In Safari werden die benutzerdefinierten Eigenschaften und eine Animation auf die Fortschrittsanzeige des Pseudoelements angewendet:

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

Bei Firefox werden die benutzerdefinierten Eigenschaften und eine Animation auch auf die Fortschrittsanzeige des Pseudoelements angewendet:

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 spielt beim <progress>-Element eine wichtige Rolle. Sie steuert den an das Element gesendeten Wert und sorgt dafür, dass im Dokument genügend Informationen für Screenreader vorhanden sind.

const state = {
  val: null
}

Die Demo enthält Schaltflächen zum Steuern des Fortschritts. Sie aktualisieren state.val und rufen dann eine Funktion zum Aktualisieren des DOM auf.

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

setProgress()

In dieser Funktion findet die UI/UX-Orchestrierung statt. Erstellen Sie zuerst eine setProgress()-Funktion. Es werden keine Parameter benötigt, da er Zugriff auf das state-Objekt, das Fortschrittselement und die <main>-Zone hat.

const setProgress = () => {
  
}

Ladestatus für die Zone <main> festlegen

Je nachdem, ob der Fortschritt abgeschlossen ist oder nicht, muss das zugehörige <main>-Element das Attribut aria-busy aktualisieren:

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

Attribute löschen, wenn die Lademenge nicht bekannt ist

Wenn der Wert unbekannt oder nicht festgelegt ist, entfernen Sie bei null die Attribute value und aria-valuenow. Dadurch wird <progress> auf Unbestimmt.

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

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

Rechenprobleme mit JavaScript-Dezimalzahlen beheben

Da ich mich dafür entschieden habe, beim Standardwert von 1 für den Fortschritt zu bleiben, verwenden die Funktionen zum Inkrementieren und Dekrementieren Dezimalzahlen. JavaScript und andere Sprachen sind darin nicht immer perfekt. Hier sehen Sie eine roundDecimals()-Funktion, mit der Sie den Überschüsse des mathematischen Ergebnisses kürzen können:

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

Runden Sie den Wert, damit er dargestellt und lesbar ist:

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 + "%"
}

Wert für Screenreader und Browserstatus festlegen

Der Wert wird an drei Stellen im DOM verwendet:

  1. Das value-Attribut des <progress>-Elements.
  2. Das Attribut aria-valuenow.
  3. Der innere Textinhalt von <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
}

Den Fokus auf den Fortschritt geben

Wenn die Werte aktualisiert sind, können sehende Nutzer die Fortschrittsänderung sehen, Nutzer von Screenreadern werden jedoch noch nicht über die Änderung informiert. Setzen Sie den Fokus auf das Element <progress>. Der Browser meldet das Update dann.

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

Screenshot der Mac OS-Voiceover-App, in der dem Nutzer der Fortschritt des Ladebalkens angezeigt wird

Fazit

Jetzt weißt du, wie ich es gemacht habe. Wie würdest du es erreichen? 🙂

Es gibt sicherlich noch ein paar Änderungen, die ich gerne vornehmen würde, wenn ich die Chance bekomme. Ich denke, es gibt Raum, die aktuelle Komponente zu bereinigen, und den Raum, um eine Komponente ohne die Stilbeschränkungen der Pseudoklasse des <progress>-Elements zu erstellen. Es lohnt sich, es auszuprobieren!

Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können.

Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.

Community-Remixe