Eine Ladebalkenkomponente erstellen

Eine grundlegende Übersicht dazu, wie Sie mit dem Element <progress> eine farblich anpassbare und barrierefreie Ladeleiste erstellen.

In diesem Beitrag möchte ich meine Gedanken dazu teilen, wie Sie mit dem Element <progress> eine farblich anpassbare und barrierefreie Ladeleiste erstellen. Probieren Sie die Demo aus und sehen Sie sich die Quelle an.

Hell und dunkel, unbestimmt, zunehmend und fertig 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 bei der Formularausfüllung, Anzeige von Informationen zum Herunterladen oder Hochladen oder auch die Anzeige, dass der Fortschritt unbekannt ist, die Arbeit aber noch aktiv ist.

Bei dieser GUI-Herausforderung wurde das vorhandene HTML-Element <progress> verwendet, um den Aufwand für die Barrierefreiheit zu reduzieren. Die Farben und Layouts gehen an die Grenzen der Anpassungsmöglichkeiten für das integrierte Element, um die Komponente zu modernisieren und besser in Designsysteme einzubinden.

Helle und dunkle Tabs in jedem Browser, die einen Überblick über das adaptive Symbol von oben nach unten bieten: Safari, Firefox, Chrome.
Demo in Firefox, Safari, iOS Safari, Chrome und Android Chrome in hellem und dunklem Design.

Markieren & Zeichnen

Ich habe das <progress>-Element in ein <label>-Element gewickelt, damit ich die expliziten Beziehungsattribute zugunsten einer impliziten Beziehung überspringen konnte. Ich habe auch ein übergeordnetes Element gekennzeichnet, das vom Ladestatus betroffen ist, damit Screenreadertechnologien 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

Bei einer impliziten Beziehung wird ein Fortschrittselement in ein Label eingeschlossen, z. B. so:

<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 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 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 nach Abschluss des Ladevorgangs ändern soll. 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 Fortschrittselement ist in Bezug auf das Design etwas schwierig. Eingebettete HTML-Elemente haben spezielle ausgeblendete Teile, die sich nur schwer auswählen lassen und oft nur eine begrenzte Anzahl von Eigenschaften bieten.

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 von 1e3px für _radius wird in wissenschaftlicher Schreibweise ausgedrückt, um eine große Zahl anzugeben. Daher wird border-radius immer gerundet. Das entspricht 1000px. Ich verwende diese Option gerne, da ich einen Wert verwenden möchte, der groß genug ist, um ihn einmal festzulegen und dann nicht mehr daran denken zu müssen. Außerdem ist es einfacher, ihn bei Bedarf zu erhöhen: Ändern Sie einfach die 3 in eine 4, dann entspricht 1e4px 10000px.1000px

overflow: hidden ist ein umstrittener Stil. Das hat einige Dinge vereinfacht, da border-radius-Werte nicht an den Titel und die Füllelemente des Titels übergeben werden mussten. Es bedeutete aber auch, dass keine untergeordneten Elemente des Fortschritts außerhalb des Elements liegen 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, der zu 100% gefüllt ist und am Ende ein Häkchen zeigt

Farbe

Der Browser verwendet eigene Farben für das Fortschrittselement und passt sich mit nur einer CSS-Eigenschaft an helle und dunkle Umgebungen an. Darauf können einige spezielle browserspezifische Auswahlkriterien aufbauen.

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

Farbe für den Fortschritt einer einzelnen Property

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 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 alle Browser

Passen Sie die Stile an, indem Sie die Teile eines <progress>-Elements auswählen, die in den einzelnen Browsern angezeigt werden. 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 stellen ::-webkit-progress-bar und ::-webkit-progress-value bereit, mit denen 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 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 die Position der Fortschrittselemente

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 eine Spurfarbe von accent-color festgelegt ist, während in iOS Safari eine hellblaue Spur verwendet wird. Das ist im Dunkelmodus dasselbe: Firefox hat einen dunklen Track, aber nicht die benutzerdefinierte Farbe, die wir festgelegt haben. Er funktioniert in Webkit-basierten Browsern.

Animation

Bei der Arbeit mit den in Browsern integrierten Pseudo-Selektoren ist oft nur eine begrenzte Anzahl von zulässigen CSS-Eigenschaften verfügbar.

Animation der Füllung des Titels

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 ebenfalls 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 ein ziemlich komplexes linear-gradient, das 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 Auswählten nicht gruppieren können.

Die Keyframes

Ziel ist eine Endlosschleife, die hin und her geht. Die Start- und End-Keyframes werden in CSS festgelegt. Für eine Animation, die immer wieder von Anfang an beginnt, ist nur ein Keyframe erforderlich, nämlich der mittlere Keyframe bei 50%.

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

Targeting auf einzelne 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 benutzerdefinierten Eigenschaften mit unbestimmtem Wert werden verwendet und die Vor- und Rückwärtsanimation 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);
}
Fortschrittsanzeige in Firefox

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

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 Mac-OS-VoiceOver-App, in der der Fortschritt der Ladeleiste für den Nutzer vorgelesen wird

Fazit

Wie würden Sie das 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 Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können.

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

Remixe der Community