Eine Ladebalkenkomponente erstellen

Ein grundlegender Überblick darüber, wie du mit dem Element <progress> eine adaptive und barrierefreie Ladeleiste erstellst.

In diesem Beitrag möchte ich Ihnen zeigen, wie Sie mit dem Element <progress> eine adaptive und barrierefreie Ladeleiste erstellen können. Probieren Sie die Demo aus und sehen Sie sich die Quelle an.

Demonstration von Hell und Dunkel, Unbestimmt, Zunahme und Vollständigkeit in Chrome.

Wenn du lieber ein Video ansiehst, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Das Element <progress> gibt Nutzern visuelles und akustisches Feedback zum Abschluss. Dieses visuelle Feedback ist in folgenden Fällen hilfreich: Fortschritt beim Ausfüllen eines Formulars, Anzeige von Informationen zum Herunterladen oder Hochladen oder auch, wenn der Fortschritt nicht bekannt ist, die Arbeit aber noch aktiv ist.

Bei dieser GUI Challenge wurde das bestehende HTML-Element <progress> verwendet, um den Aufwand für die Barrierefreiheit zu verringern. Die Farben und Layouts erweitern die Grenzen der Anpassung für das integrierte Element, um die Komponente zu modernisieren und besser in Designsysteme zu integrieren.

Helle und dunkle Tabs in jedem Browser, die einen Überblick über das adaptive Symbol von oben nach unten geben: Safari, Firefox, Chrome.
Die Demo wird für Firefox, Safari, iOS Safari, Chrome und Android Chrome in hellen und dunklen Schemata dargestellt.

Markieren & Zeichnen

Ich habe das <progress>-Element in <label> eingebunden, damit ich die expliziten Beziehungsattribute zugunsten einer impliziten Beziehung überspringen kann. Ich habe auch ein übergeordnetes Element gekennzeichnet, das vom Ladestatus betroffen ist, damit Screenreader diese Informationen an einen Nutzer weitergeben können.

<progress></progress>

Wenn kein value vorhanden ist, ist der Fortschritt des Elements unbestimmt. Das Attribut max hat standardmäßig den Wert 1. Der Fortschritt liegt also zwischen 0 und 1. Wenn Sie max beispielsweise auf 100 festlegen, wird der Bereich auf 0–100 festgelegt. Ich habe mich dafür entschieden, die Grenzen von 0 und 1 einzuhalten und Fortschrittswerte in 0,5 oder 50 % umzuwandeln.

Fortschritt mit Label

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

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

In meiner Demo habe ich das Label nur für Screenreader eingefügt. Dazu fügen Sie den Labeltext in ein <span> ein und wenden einige Stile darauf an, damit er nicht mehr auf dem Bildschirm zu sehen ist:

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

Mit folgendem CSS-Code 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 DevTools, in dem das Element „Nur für Bildschirm bereit“ zu sehen ist

Vom Ladevorgang betroffenes Gebiet

Wenn Sie gut sehen, ist es oft einfach, eine Fortschrittsanzeige mit zugehörigen Elementen und Seitenbereichen zu verknüpfen. Für sehbehinderte Nutzer ist das jedoch nicht so klar. Sie können das Problem beheben, indem Sie das Attribut aria-busy dem obersten Element zuweisen, das sich ändern soll, wenn der Ladevorgang abgeschlossen ist. Geben Sie außerdem mit aria-describedby den Zusammenhang zwischen dem Fortschritt und der Ladezone an.

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

Wechseln Sie in JavaScript zu Beginn der Aufgabe von aria-busy zu true und am Ende zu false.

Hinzugefügte ARIA-Attribute

Die implizite Rolle eines <progress>-Elements ist progressbar. Ich habe sie für Browser explizit angegeben, die diese implizite Rolle nicht haben. Ich habe außerdem das Attribut indeterminate hinzugefügt, um dem Element explizit den Status „Unbekannt“ zuzuweisen. Das ist verständlicher als zu sehen, 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", um das Fortschrittselement über JavaScript fokussierbar zu machen. Das ist für Screenreader wichtig, da der Nutzer bei Änderungen des Fortschritts mitgeteilt bekommt, wie weit der aktualisierte Fortschritt ist.

Stile

Das Progress-Element ist etwas komplizierter. Integrierte HTML-Elemente haben besondere versteckte Teile, die schwer auszuwählen sind und oft nur eine begrenzte Anzahl von Eigenschaften bieten, die festgelegt werden können.

Layout

Die Layoutstile sollen eine gewisse Flexibilität bei der Größe und Labelposition des Fortschrittselements ermöglichen. Es wird ein spezieller Abschlussstatus hinzugefügt, der ein nützliches, aber nicht erforderliches zusätzliches visuelles Signal sein kann.

<progress> Layout

Die Breite des Fortschrittselements bleibt unverändert, sodass es sich entsprechend dem benötigten Platz im Design verkleinern und vergrößern kann. Die integrierten Stile werden entfernt, indem appearance und border auf none gesetzt werden. So kann das Element plattformübergreifend normalisiert werden, da jeder Browser eigene Stile für das 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 1e3px für _radius verwendet die wissenschaftliche Schreibweise, um eine große Zahl auszudrücken, sodass border-radius immer gerundet wird. Das entspricht 1000px. Ich verwende diese Option gerne, da ich einen Wert verwenden möchte, der groß genug ist, damit ich ihn einmal festlegen und dann vergessen kann. Außerdem ist es einfacher, ihn bei Bedarf noch zu erhöhen: Ändern Sie einfach die 3 in eine 4, dann entspricht 1e4px 10000px.1000px

overflow: hidden ist ein umstrittener Stil. Dadurch wurde einiges vereinfacht, z. B. musste keine border-radius-Werte an den Track übergeben und Füllelemente verfolgt werden. Es bedeutete aber auch, dass keine untergeordneten Elemente des Fortschritts außerhalb des Elements angesiedelt sein konnten. Eine weitere Iteration dieses benutzerdefinierten Fortschrittselements könnte ohne overflow: hidden erfolgen und bietet möglicherweise einige Möglichkeiten für Animationen oder bessere Abschlussstatus.

Vorgang abgeschlossen

CSS-Selektoren erledigen hier die harte Arbeit, indem sie den Maximalwert 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. Dies dient als zusätzliche visuelle Aufforderung, dass der Vorgang abgeschlossen ist.

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 der Ladebalken bei 100 % mit einem Häkchen am Ende

Farbe

Der Browser verwendet für das Fortschrittselement eigene Farben und ist mit nur einer CSS-Eigenschaft an hell und dunkel angepasst. Dies kann mit einigen speziellen browserspezifischen Selektoren aufgebaut werden.

Helle und dunkle Browserstyles

Wenn Sie für Ihre Website ein dunkles und ein helles adaptives <progress>-Element aktivieren möchten, ist nur color-scheme erforderlich.

progress {
  color-scheme: light dark;
}

Füllfarbe des Fortschritts einer einzelnen Unterkunft

Verwenden Sie accent-color, um ein <progress>-Element zu färben.

progress {
  accent-color: rebeccapurple;
}

Die Hintergrundfarbe des Tracks ändert sich je nach accent-color von hell zu dunkel. Der Browser sorgt für einen guten Kontrast: Sehr gut.

Vollständig benutzerdefinierte helle und dunkle Farben

Legen Sie zwei benutzerdefinierte Eigenschaften für das Element <progress> fest, eine für die Titelfarbe und eine für die Farbe des Titelfortschritts. Gib in der Mediaabfrage prefers-color-scheme neue Farbwerte für den Titel und den Titelfortschritt an.

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

Wir haben dem Element einen negativen Tabindex zugewiesen, damit es programmatisch fokussiert werden kann. Verwenden Sie :focus-visible, um den Fokus anzupassen und den intelligenteren Fokusring zu aktivieren. In diesem Fall wird der Fokusring nicht durch einen Mausklick und Fokus gesetzt, sondern nur durch Tastaturklicks. Im YouTube-Video wird das Thema ausführlicher behandelt.

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

Screenshot der Ladeleiste mit einem Fokusring drumherum Die Farben stimmen überein.

Benutzerdefinierte Stile für verschiedene Browser

Passen Sie die Stile an, indem Sie die Teile eines <progress>-Elements auswählen, die für jeden Browser sichtbar sind. Das Fortschrittselement besteht aus einem einzelnen Tag, das jedoch aus mehreren untergeordneten Elementen besteht, die über CSS-Pseudoselektoren freigelegt werden. Wenn Sie die Einstellung aktivieren, werden Ihnen in den Chrome-Entwicklertools die folgenden Elemente angezeigt:

  1. Klicken Sie mit der rechten Maustaste auf die Seite und wählen Sie Element untersuchen aus, um die Entwicklertools zu öffnen.
  2. Klicken Sie rechts oben im DevTools-Fenster auf das Zahnradsymbol für die Einstellungen.
  3. Klicken Sie unter der Überschrift Elemente auf das Kästchen User-Agent-Schatten-DOM anzeigen und aktivieren Sie es.

Screenshot, in dem zu sehen ist, wo in den DevTools das User-Agent-Shadow-DOM aktiviert werden kann

Safari- und Chromium-Stile

WebKit-basierte Browser wie Safari und Chromium bieten ::-webkit-progress-bar und ::-webkit-progress-value an, 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 Umgebungen anpassen.

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

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

Screenshot, der die inneren Elemente des Fortschrittselements zeigt

Firefox-Stile

In Firefox wird der Pseudo-Selektor ::-moz-progress-bar nur für das Element <progress> angezeigt. 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 Informationen dazu, wo Sie die Bestandteile des Fortschrittselements finden

Screenshot der Debugging-Ecke, in der die Ladeleiste für Safari, iOS Safari, Firefox, Chrome und Chrome für Android angezeigt wird.

Beachten Sie, dass in Firefox die Track-Farbe accent-color und in iOS Safari eine hellblaue Spur verwendet wird. Im dunklen Modus ist das Gleiche: Firefox hat einen dunklen Spuren, aber nicht die von uns festgelegte benutzerdefinierte Farbe, und es funktioniert in WebKit-basierten Browsern.

Animation

Wenn Sie mit im Browser integrierten Pseudoselektoren arbeiten, steht dafür häufig nur eine begrenzte Anzahl zulässiger CSS-Eigenschaften zur Verfügung.

Animation der Füllung des Tracks

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

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

:indeterminate-Status animieren

Hier werde ich etwas kreativer, damit ich eine Animation erstellen kann. Es wird ein Pseudo-Element für Chromium erstellt und ein Farbverlauf angewendet, der für alle drei Browser hin und her animiert wird.

Benutzerdefinierte Eigenschaften

Benutzerdefinierte Properties eignen sich für viele Zwecke. Eine meiner Lieblingsfunktionen ist es, einem ansonsten magisch aussehenden CSS-Wert einen Namen zu geben. Im Folgenden finden Sie eine ziemlich komplexe linear-gradient, die aber einen schönen Namen hat. 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 Properties tragen auch dazu bei, dass der Code DRY bleibt, da wir diese browserspezifischen Auswahlkriterien nicht gruppieren können.

Die Keyframes

Ziel ist eine Endlosschleife, die hin und her geht. Die Start- und End-Keyframes werden in CSS festgelegt. Sie benötigen nur einen Keyframe – den mittleren Keyframe unter 50% –, um eine Animation zu erstellen, die immer wieder zum Ausgangspunkt zurückkehrt.

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

Targeting auf jeden Browser

Nicht jeder Browser erlaubt das Erstellen von Pseudoelementen im <progress>-Element selbst oder die Animation der Fortschrittsanzeige. Mehr Browser unterstützen die Animation des Tracks als ein Pseudo-Element. Daher wechsele ich von Pseudo-Elementen als Basis zu animierten Balken.

Chromium-Pseudoelement

In Chromium ist das Pseudo-Element ::after mit einer Position zulässig, um das Element abzudecken. Die unbestimmten benutzerdefinierten Eigenschaften werden verwendet und die Rück- und Rückanimation 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);
}
Fortschrittsanzeige in Safari

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

In 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 Element <progress> 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 bietet 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()

Hier erfolgt die UI/UX-Orchestrierung. Erstellen Sie zuerst eine setProgress()-Funktion. Es sind keine Parameter erforderlich, da es 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 für das Attribut aria-busy aktualisiert werden:

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

Attribute löschen, wenn die Lademenge unbekannt ist

Wenn der Wert unbekannt oder nicht festgelegt ist, null in dieser Verwendung, entfernen Sie die Attribute value und aria-valuenow. Dadurch wird der Wert für <progress> auf „Unbestimmt“ gesetzt.

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

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

Probleme mit Dezimalrechnungen in JavaScript beheben

Da ich mich für das Standardmaximum von 1 entschieden habe, werden in der Demo für die Inkrement- und Dekrementfunktionen Dezimalzahlen verwendet. JavaScript und andere Sprachen eignen sich dafür nicht immer gut. Hier ist eine roundDecimals()-Funktion, die den Überschuss des mathematischen Ergebnisses abschneidet:

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

Runden Sie den Wert so, dass er präsentiert werden kann und gut 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 Fortschritt in den Mittelpunkt stellen

Nachdem die Werte aktualisiert wurden, sehen sehende Nutzer die Fortschrittsänderung, aber Nutzer von Screenreadern werden noch nicht über die Änderung informiert. Wenn Sie das Element <progress> hervorheben, wird die Aktualisierung im Browser angekündigt.

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 Voice Over-App unter Mac OS, der dem Nutzer den Fortschritt des Ladebalkens vorliest.

Fazit

Wie würden Sie es machen?

Wenn ich noch einmal die Chance bekommen würde, würde ich einige Dinge anders machen. Ich denke, dass die aktuelle Komponente optimiert werden kann und dass es möglich ist, eine Komponente ohne die Einschränkungen des Pseudoklassenstils des <progress>-Elements zu erstellen. Es lohnt sich, sie auszuprobieren.

Lassen Sie uns unsere Herangehensweisen diversifizieren und alle Möglichkeiten kennenlernen, wie wir das Web entwickeln können.

Erstelle eine Demo, tweete mir Links und ich füge sie unten in den Abschnitt „Community-Remixe“ hinzu.

Community-Remixe