Eine Ladebalkenkomponente erstellen

Eine grundlegende Übersicht darüber, wie Sie mit dem <progress>-Element eine farbadaptive und barrierefreie Ladeleiste erstellen.

In diesem Beitrag möchte ich Ihnen zeigen, wie Sie mit dem <progress>-Element eine farbadaptive und barrierefreie Ladeleiste erstellen können. Demo ansehen und Quellcode ansehen.

Helle und dunkle, unbestimmte, ansteigende und abgeschlossene Fortschrittsanzeigen in Chrome.

Wenn du lieber ein Video ansehen möchtest, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Das Element <progress> bietet Nutzern visuelles und akustisches Feedback zum Abschluss. Dieses visuelle Feedback ist in folgenden Szenarien nützlich: Fortschritt durch ein Formular, Anzeige von Download- oder Uploadinformationen oder sogar Anzeige, dass der Fortschritt unbekannt ist, aber die Arbeit noch aktiv ist.

Bei dieser GUI-Challenge wurde das vorhandene HTML-Element <progress> verwendet, um den Aufwand für die Barrierefreiheit zu verringern. Die Farben und Layouts gehen über die Anpassungsmöglichkeiten des integrierten Elements hinaus, um die Komponente zu modernisieren und besser in Designsysteme einzufügen.

Helle und dunkle Tabs in jedem Browser mit einer Übersicht des adaptiven Symbols von oben nach unten: Safari, Firefox, Chrome.
Die Demo wird in Firefox, Safari, iOS Safari, Chrome und Android Chrome im hellen und dunklen Design gezeigt.

Markieren & Zeichnen

Ich habe das <progress>-Element in ein <label>-Element eingeschlossen, um explizite Beziehungsattribute zugunsten einer impliziten Beziehung zu überspringen. Außerdem habe ich ein übergeordnetes Element mit dem Ladezustand gekennzeichnet, damit Screenreader diese Informationen an den 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 gesetzt. Ich habe mich entschieden, die Grenzwerte 0 und 1 beizubehalten und Fortschrittswerte in 0,5 oder 50 % zu übersetzen.

Fortschritt mit Label

Bei einer impliziten Beziehung wird ein Fortschrittselement so in ein Label eingeschlossen:

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

In meiner Demo habe ich das Label nur für Screenreader ausgewählt. Dazu wird der Labeltext in ein <span>-Tag eingeschlossen und es werden einige Formatierungen angewendet, damit er effektiv nicht auf dem Bildschirm angezeigt wird:

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

Mit dem folgenden begleitenden 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, auf dem das Element „Bildschirm bereit“ zu sehen ist.

Bereich, der vom Ladestatus betroffen ist

Wenn Sie eine gute Sehkraft haben, können Sie einen Fortschrittsindikator leicht mit zugehörigen Elementen und Seitenbereichen in Verbindung bringen. Für Nutzer mit Sehbeeinträchtigungen ist das jedoch nicht so einfach. Sie können dies verbessern, indem Sie das Attribut aria-busy dem obersten Element zuweisen, das sich ändert, wenn das Laden abgeschlossen ist. 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>

Schalten Sie in JavaScript zu Beginn der Aufgabe aria-busy auf true und nach Abschluss auf false um.

Hinzufügen von Aria-Attributen

Die implizite Rolle eines <progress>-Elements ist progressbar. Ich habe sie jedoch für Browser, die diese implizite Rolle nicht unterstützen, explizit angegeben. Außerdem habe ich das Attribut indeterminate hinzugefügt, um das Element explizit in den Status „Unbekannt“ zu versetzen. Das ist eindeutiger, als wenn man feststellt, 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 wichtig für Screenreader, da der Nutzer so erfährt, wie weit der aktualisierte Fortschritt ist.

Stile

Das Fortschrittselement ist in Bezug auf die Gestaltung etwas kompliziert. Integrierte HTML-Elemente haben spezielle verborgene 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 des Fortschrittselements und der Position der Beschriftung ermöglichen. Es wurde ein spezieller Abschlussstatus hinzugefügt, der als nützliches, aber nicht erforderliches zusätzliches visuelles Signal dienen kann.

<progress> Layout

Die Breite des Fortschrittselements bleibt unverändert, sodass es je nach Bedarf im Design verkleinert und vergrößert werden kann. Die integrierten Stile werden entfernt, indem appearance und border auf none gesetzt werden. So kann das Element browserü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 von 1e3px für _radius wird in wissenschaftlicher Schreibweise ausgedrückt, um eine große Zahl darzustellen. Daher wird border-radius immer gerundet. Dies entspricht 1000px. Ich verwende diese Option gern, weil ich einen Wert verwenden möchte, der so groß ist, dass ich ihn einmal festlegen und dann vergessen kann. Außerdem ist er kürzer als 1000px. Bei Bedarf lässt er sich auch ganz einfach noch größer machen: Ändern Sie einfach die 3 in eine 4. Dann entspricht 1e4px 10000px.

overflow: hidden wird verwendet und war schon immer umstritten. Das hat einige Dinge vereinfacht, z. B. mussten keine border-radius-Werte an den Track und die Track-Füllelemente übergeben werden. Es bedeutete aber auch, dass keine untergeordneten Elemente des Fortschritts außerhalb des Elements vorhanden sein konnten. Eine weitere Iteration dieses benutzerdefinierten Fortschrittselements könnte ohne overflow: hidden erfolgen. Das könnte einige Möglichkeiten für Animationen oder bessere Abschlussstatus eröffnen.

Vorgang abgeschlossen

CSS-Selektoren vergleichen den Maximalwert mit dem Wert. Wenn sie übereinstimmen, ist der Fortschritt abgeschlossen. Wenn der Vorgang abgeschlossen ist, wird ein Pseudoelement generiert und an das Ende des Fortschrittselements angehängt. So wird ein zusätzlicher visueller Hinweis auf den Abschluss gegeben.

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 bei 100 %, am Ende ist ein Häkchen zu sehen.

Farbe

Der Browser stellt eigene Farben für das Fortschrittselement bereit, die sich mit nur einer CSS-Eigenschaft an helle und dunkle Designs anpassen lassen. Darauf lässt sich mit einigen speziellen browserspezifischen Selektoren aufbauen.

Helle und dunkle Browserstile

Um Ihre Website für ein adaptives <progress>-Element für helle und dunkle Designs zu aktivieren, ist nur color-scheme erforderlich.

progress {
  color-scheme: light dark;
}

Farbe für Fortschrittsanzeige für einzelne Unterkunft

Verwenden Sie accent-color, um ein <progress>-Element zu tönen.

progress {
  accent-color: rebeccapurple;
}

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

Vollständig benutzerdefinierte helle und dunkle Farben

Legen Sie zwei benutzerdefinierte Eigenschaften für das <progress>-Element fest, eine für die Farbe des Tracks und eine für die Farbe des Trackfortschritts. Geben Sie in der Media-Anfrage prefers-color-scheme neue Farbwerte für den Track und den Fortschritt des Tracks 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

Vorhin haben wir dem Element einen negativen Tabindex zugewiesen, damit es programmatisch fokussiert werden kann. Mit :focus-visible können Sie den Fokus anpassen und den intelligenten Fokusringstil aktivieren. Bei einem Mausklick und Fokus wird der Fokusring nicht angezeigt, bei Tastaturklicks jedoch schon. Das YouTube-Video enthält weitere Informationen dazu.

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

Screenshot des Ladebalkens mit einem Fokusring darum herum. Alle Farben stimmen überein.

Browserübergreifende benutzerdefinierte Stile

Passen Sie die Stile an, indem Sie die Teile eines <progress>-Elements auswählen, die von den einzelnen Browsern bereitgestellt werden. Das „progress“-Element ist ein einzelnes Tag, besteht aber aus mehreren untergeordneten Elementen, die über CSS-Pseudoselektoren verfügbar gemacht werden. In den Chrome-Entwicklertools werden diese Elemente angezeigt, wenn Sie die Einstellung aktivieren:

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

Screenshot, der zeigt, wo in den Entwicklertools das Shadow-DOM des User-Agents aktiviert werden kann.

Safari- und Chromium-Stile

WebKit-basierte Browser wie Safari und Chromium stellen ::-webkit-progress-bar und ::-webkit-progress-value zur Verfügung, wodurch eine Teilmenge von CSS verwendet werden kann. Legen Sie background-color vorerst mit den zuvor erstellten benutzerdefinierten Eigenschaften fest, die sich an helle und dunkle Designs 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 Fortschrittselements.

Firefox-Stile

In Firefox wird der Pseudoselektor ::-moz-progress-bar nur für das Element <progress> verfügbar gemacht. Das bedeutet auch, dass wir den Track nicht direkt tönen können.

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

Screenshot von Firefox mit den Teilen des Fortschrittselements.

Screenshot der Debugging Corner, auf dem die Ladeleiste in Safari, iOS Safari, Firefox, Chrome und Chrome unter Android zu sehen ist.

In Firefox ist die Farbe des Tracks über accent-color festgelegt, in iOS Safari ist der Track hellblau. Im dunklen Modus ist es dasselbe: Firefox hat einen dunklen Track, aber nicht die benutzerdefinierte Farbe, die wir festgelegt haben. In Webkit-basierten Browsern funktioniert es.

Animation

Bei der Verwendung von integrierten Browser-Pseudoselektoren ist oft nur eine begrenzte Anzahl von zulässigen CSS-Eigenschaften verfügbar.

Animation des sich füllenden Tracks

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

/*  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 einfügen kann. Für Chromium wird ein Pseudoelement erstellt und ein Farbverlauf angewendet, der in allen drei Browsern hin und her animiert wird.

Die benutzerdefinierten Eigenschaften

Benutzerdefinierte Eigenschaften sind für viele Dinge nützlich. Am liebsten verwende ich sie, um einem ansonsten magisch aussehenden CSS-Wert einen Namen zu geben. Im Folgenden sehen Sie eine ziemlich komplexe linear-gradient 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 gruppieren können.

Die Keyframes

Das Ziel ist eine Endlosanimation, die vor- und zurückläuft. Die Start- und End-Keyframes werden in CSS festgelegt. Für eine Animation, die immer wieder zum Ausgangspunkt zurückkehrt, ist nur ein Keyframe erforderlich: der mittlere Keyframe bei 50%.

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

Targeting auf die einzelnen Browser

Nicht in jedem Browser können Pseudo-Elemente für das <progress>-Element erstellt oder die Fortschrittsanzeige animiert werden. Mehr Browser unterstützen die Animation des Tracks als die eines Pseudoelements. Daher verwende ich keine Pseudoelemente mehr als Grundlage, sondern animiere Balken.

Chromium-Pseudoelement

In Chromium ist das Pseudoelement ::after zulässig, wenn es mit einer Position verwendet wird, um das Element abzudecken. Die unbestimmten benutzerdefinierten Eigenschaften werden verwendet und die Animation 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 eine wichtige Rolle beim <progress>-Element. Damit wird der an das Element gesendete Wert gesteuert und dafür gesorgt, dass im Dokument genügend Informationen für Screenreader vorhanden sind.

const state = {
  val: null
}

In der Demo gibt es 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 sind keine Parameter erforderlich, da die Funktion Zugriff auf das state-Objekt, das Fortschrittselement und die <main>-Zone hat.

const setProgress = () => {
  
}

Ladezustand für die Zone <main> festlegen

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

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

Attribute löschen, wenn der Ladebetrag unbekannt ist

Wenn der Wert unbekannt oder nicht festgelegt ist, entfernen Sie die Attribute value und aria-valuenow.null Dadurch wird der Status von <progress> auf „Unbestimmt“ geändert.

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 der Dezimalarithmetik in JavaScript beheben

Da ich mich für den Standardwert 1 als maximalen Fortschritt entschieden habe, verwenden die Inkrement- und Dekrementfunktionen der Demo Dezimalzahlen. JavaScript und andere Sprachen sind dafür nicht immer gut geeignet. Hier ist eine roundDecimals()-Funktion, mit der das Ergebnis der mathematischen Operation gekürzt wird:

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

Runden Sie den Wert, damit er präsentiert werden kann 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 Attribut value des Elements <progress>.
  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
}

Fokus auf den Fortschritt legen

Wenn die Werte aktualisiert wurden, sehen Nutzer ohne Sehbeeinträchtigung die Änderung des Fortschritts. Nutzer von Screenreadern erhalten die Änderung jedoch noch nicht als Ansage. Fokussieren Sie das <progress>-Element. Der Browser gibt die Aktualisierung dann aus.

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 VoiceOver-App unter Mac OS, die dem Nutzer den Fortschritt der Ladeleiste vorliest.

Fazit

Jetzt wissen Sie, wie ich es gemacht habe. Wie würden Sie vorgehen? 🙂

Es gibt sicherlich ein paar Änderungen, die ich vornehmen würde, wenn ich noch einmal die Chance dazu hätte. Ich denke, dass die aktuelle Komponente verbessert werden kann und dass es möglich ist, eine Komponente ohne die Einschränkungen der Pseudoklassenstile des <progress>-Elements zu erstellen. Es lohnt sich, sich das genauer anzusehen.

Wir möchten unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, die das Web bietet.

Erstelle eine Demo, schick mir einen Tweet mit den Links und ich füge sie unten im Bereich „Community-Remixe“ hinzu.

Community-Remixe