Creazione di un componente della barra di caricamento

Una panoramica di base su come creare una barra di caricamento adattabile al colore e accessibile con l'elemento <progress>.

In questo post voglio condividere alcune idee su come creare una barra di caricamento adattabile al colore e accessibile con l'elemento <progress>. Prova la demo e visualizza il codice sorgente.

Chiaro e scuro, indeterminato, crescente e completato in prova su Chrome.

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

L'elemento <progress> fornisce agli utenti un feedback visivo e udibile sul completamento. Questo feedback visivo è prezioso per scenari come l'avanzamento in un modulo, la visualizzazione di informazioni in merito a download o caricamento o persino che l'importo dell'avanzamento non è noto, ma il lavoro è ancora attivo.

Questo sfide della GUI ha utilizzato l'elemento HTML <progress> esistente per risparmiare un po' di impegno in termini di accessibilità. I colori e i layout spingono i limiti della personalizzazione dell'elemento integrato per modernizzare il componente e adattarlo meglio ai sistemi di progettazione.

Schede chiare e scure di ogni browser con una panoramica dell&#39;icona adattiva dall&#39;alto verso il basso: Safari, Firefox, Chrome.
La demo è stata mostrata su Firefox, Safari, Safari per iOS, Chrome e Chrome per Android in modalità chiara e scura.

Segni e linee

Ho scelto di aggregare l'elemento <progress> in un <label> in modo da ignorare gli attributi di relazione esplicita a favore di una relazione implicita. Ho anche etichettato un elemento principale interessato dallo stato di caricamento, in modo che le tecnologie di screen reader possano ritrasmettere queste informazioni a un utente.

<progress></progress>

Se non è presente value, l'avanzamento dell'elemento è indeterminato. Il valore predefinito dell'attributo max è 1, quindi l'avanzamento è compreso tra 0 e 1. Impostando max su 100, ad esempio, l'intervallo viene impostato su 0-100. Ho scelto di rispettare i limiti di 0 e 1, traducendo i valori di avanzamento in 0,5 o 50%.

Avanzamento del wrapping delle etichette

In una relazione implicita, un elemento di avanzamento è aggregato da un'etichetta come la seguente:

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

Nella mia demo ho scelto di includere l'etichetta solo per screen reader. Per farlo, inserisci il testo dell'etichetta in un <span> e applica alcuni stili in modo che non sia visibile sullo schermo:

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

Con il seguente CSS aggiuntivo di 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 degli strumenti per gli sviluppatori che mostrano l&#39;elemento solo schermo pronto.

Area interessata dall'avanzamento del caricamento

Se hai una vista sana, può essere facile associare un indicatore di avanzamento con elementi correlati e aree della pagina, ma per gli utenti con disabilità visive non è così chiaro. Migliora la situazione assegnando l'attributo aria-busy all'elemento più in alto, che cambierà al termine del caricamento. Inoltre, indica un rapporto tra l'avanzamento e la zona di caricamento con aria-describedby.

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

Da JavaScript, passa da aria-busy a true all'inizio dell'attività e a false al termine dell'attività.

Aggiunte attributi Aria

Sebbene il ruolo implicito di un elemento <progress> sia progressbar, l'ho reso esplicito per i browser che non dispongono di questo ruolo implicito. Ho anche aggiunto l'attributo indeterminate per impostare esplicitamente lo stato dell'elemento su Sconosciuto, il che è più chiaro rispetto all'osservazione che l'elemento non ha value impostato.

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

Utilizza tabindex="-1" per rendere l'elemento di avanzamento selezionabile da JavaScript. Questo è importante per la tecnologia degli screen reader, poiché impostare lo stato attivo sull'avanzamento quando cambia l'avanzamento comunicherà all'utente quanto è stato raggiunto l'avanzamento aggiornato.

Stili

L'elemento di avanzamento è un po' complicato per quanto riguarda lo stile. Gli elementi HTML integrati hanno parti nascoste speciali che possono essere difficili da selezionare e spesso offrono solo un insieme limitato di proprietà da impostare.

Layout

Gli stili di layout hanno lo scopo di consentire una certa flessibilità nelle dimensioni e nella posizione dell'etichetta dell'elemento di avanzamento. Viene aggiunto uno stato di completamento speciale che può essere un indicatore visivo aggiuntivo utile, ma non obbligatorio.

<progress> Layout

La larghezza dell'elemento di avanzamento viene lasciata invariata in modo che possa ridursi e aumentare in base allo spazio necessario nel design. Gli stili integrati vengono rimossi impostando appearance e border su none. In questo modo l'elemento può essere normalizzato nei vari browser, poiché ogni browser ha i propri stili.

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

Il valore di 1e3px per _radius utilizza la notazione scientifica del numero per esprimere un numero elevato, pertanto border-radius viene sempre arrotondato. È equivalente a 1000px. Mi piace utilizzarlo perché il mio obiettivo è usare un valore sufficientemente elevato da poterlo impostare e dimenticarlo (ed è più breve da scrivere di 1000px). È anche facile aumentarlo ulteriormente, se necessario: basta sostituire il 3 con un 4, quindi 1e4px è equivalente a 10000px.

overflow: hidden è uno stile controverso e viene utilizzato. Ha semplificato alcune cose, ad esempio non è necessario trasferire i valori border-radius all'elemento traccia e monitorare gli elementi di riempimento, ma ciò significava anche che nessun elemento secondario dell'avanzamento poteva vivere al di fuori dell'elemento. Un'altra iterazione di questo elemento di avanzamento personalizzato potrebbe essere eseguita senza overflow: hidden e potrebbe aprire alcune opportunità per animazioni o stati di completamento migliori.

Operazione completata

I selettori CSS fanno il lavoro più difficile confrontando il valore massimo con il valore e, se corrispondono, l'avanzamento è completo. Al termine, viene generato uno pseudo-elemento e aggiunto alla fine dell'elemento di avanzamento, fornendo un ulteriore indicatore visivo del completamento.

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 della barra di caricamento al 100% e un segno di spunta alla fine.

Colore

Il browser offre i propri colori per l'elemento di avanzamento ed è adatto a modalità chiare e scure con una sola proprietà CSS. Questa funzionalità può essere ampliata con alcuni selezionatori speciali specifici del browser.

Stili del browser chiaro e scuro

Per attivare un elemento <progress> adattabile scuro e chiaro sul tuo sito, è sufficiente color-scheme.

progress {
  color-scheme: light dark;
}

Colore di riempimento dell'avanzamento di una singola proprietà

Per colorare un elemento <progress>, utilizza accent-color.

progress {
  accent-color: rebeccapurple;
}

Nota che il colore di sfondo della traccia cambia da chiaro a scuro a seconda del valore di accent-color. Il browser garantisce un contrasto adeguato: molto pratico.

Colori chiari e scuri completamente personalizzati

Imposta due proprietà personalizzate sull'elemento <progress>, una per il colore del percorso e l'altra per il colore di avanzamento del tracciamento. All'interno della query media prefers-color-scheme, fornisci nuovi valori di colore per la traccia e l'avanzamento della traccia.

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

Stili di messa a fuoco

In precedenza abbiamo assegnato all'elemento un indice di tabulazione negativo in modo che potesse essere incentrato sulla programmazione. Usa :focus-visible per personalizzare la messa a fuoco in modo da attivare lo stile più intelligente dell'anello di messa a fuoco. In questo modo, un clic del mouse e l'evidenziazione dell'elemento attivo non mostreranno l'anello di evidenza, ma i clic della tastiera sì. Il video di YouTube approfondisce questo argomento e merita di essere guardato.

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

Screenshot della barra di caricamento con intorno un anello di messa a fuoco. I colori corrispondono.

Stili personalizzati su più browser

Personalizza gli stili selezionando le parti di un elemento <progress> esposte da ciascun browser. L'utilizzo dell'elemento progress è un singolo tag, ma è formato da pochi elementi secondari esposti tramite pseudoselettori CSS. Chrome DevTools ti mostrerà questi elementi se attivi l'impostazione:

  1. Fai clic con il tasto destro del mouse sulla pagina e seleziona Ispeziona elemento per visualizzare DevTools.
  2. Fai clic sull'icona a forma di ingranaggio delle impostazioni nell'angolo in alto a destra della finestra DevTools.
  3. Sotto la voce Elementi, trova e abilita la casella di controllo Mostra DOM shadow dell'agente utente.

Screenshot che mostra dove abilitare l&#39;esposizione del DOM shadow dello user agent in DevTools.

Stili Safari e Chromium

I browser basati su WebKit, come Safari e Chromium, espongono ::-webkit-progress-bar e ::-webkit-progress-value, che consentono di utilizzare un sottoinsieme di CSS. Per il momento, imposta background-color utilizzando le proprietà personalizzate create in precedenza, che si adattano a modalità chiara e scura.

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

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

Screenshot che mostra gli elementi interni dell&#39;elemento di avanzamento.

Stili di Firefox

Firefox espone lo pseudo selettore ::-moz-progress-bar solo sull'elemento <progress>. Ciò significa anche che non possiamo colorare direttamente la traccia.

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

Screenshot di Firefox e dove trovare le parti dell&#39;elemento di avanzamento.

Screenshot dell&#39;angolo di debug in cui è visualizzata la barra di caricamento in Safari, iOS Safari, Firefox, Chrome e Chrome su Android.

Tieni presente che Firefox ha un colore traccia impostato su accent-color, mentre Safari per iOS ha una traccia azzurra. È lo stesso in modalità Buio: Firefox ha una traccia scura, ma non il colore personalizzato che abbiamo impostato e funziona nei browser basati su Webkit.

Animazione

Quando si utilizzano gli pseudo-selettori integrati del browser, spesso si ha a che fare con un insieme limitato di proprietà CSS consentite.

Animazione del riempimento della traccia

L'aggiunta di una transizione all'elemento inline-size dell'elemento di avanzamento funziona per Chromium, ma non per Safari. Inoltre, Firefox non utilizza una proprietà di transizione per il suo ::-moz-progress-bar.

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

Animazione dello stato :indeterminate

Qui posso ottenere un po' più di creatività in modo da poter fornire un'animazione. Viene creato un pseudo-elemento per Chromium e viene applicato un gradiente animato avanti e indietro per tutti e tre i browser.

Le proprietà personalizzate

Le proprietà personalizzate sono ottime per molti aspetti, ma una delle mie preferite è semplicemente assegnare un nome a un valore CSS altrimenti magico. Di seguito è riportato un linear-gradient abbastanza complesso, ma con un bel nome. Lo scopo e i casi d'uso devono essere compresi chiaramente.

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

Le proprietà personalizzate contribuiranno inoltre a mantenere il codice DRY, poiché ancora una volta non possiamo agrupare questi selettori specifici del browser.

I fotogrammi chiave

L&#39;obiettivo è un&#39;animazione infinita che va avanti e indietro. I fotogrammi chiave di inizio e fine saranno impostati in CSS. Per creare un'animazione che ritorna sempre al punto di partenza, è necessario un solo fotogramma chiave, quello intermedio in 50%.

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

Scegliere come target ogni browser

Non tutti i browser consentono la creazione di pseudo-elementi nell'elemento <progress> stessa o l'animazione della barra di avanzamento. Più browser supportano l'animazione della traccia rispetto a uno pseudo-elemento, quindi eseguo l'upgrade dagli pseudo-elementi come base alle barre animate.

Pseudoelemento Chromium

Chromium consente l'uso dell'elemento pseudo: ::after con una posizione per coprire l'elemento. Vengono utilizzate le proprietà personalizzate indeterminate e l&#39;animazione avanti e indietro funziona molto bene.

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);
}
Barra di avanzamento di Safari

Per Safari, le proprietà personalizzate e un'animazione vengono applicate alla barra di avanzamento dell'pseudo-elemento:

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

Per Firefox, le proprietà personalizzate e un'animazione vengono applicate anche alla barra di avanzamento dell'elemento pseudo:

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 svolge un ruolo importante con l'elemento <progress>. Controlla il valore inviato all'elemento e garantisce che nel documento siano presenti informazioni sufficienti per gli screen reader.

const state = {
  val: null
}

La demo offre pulsanti per controllare l'avanzamento; aggiornano state.val e quindi chiamano una funzione per aggiornare il DOM.

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

setProgress()

In questa funzione avviene l'orchestrazione dell'UI/UX. Per iniziare, crea una funzionesetProgress(). Non sono necessari parametri perché ha accesso all'oggetto state, all'elemento di avanzamento e alla zona <main>.

const setProgress = () => {
  
}

Impostazione dello stato di caricamento nella zona <main>

A seconda che l'avanzamento sia completo o meno, l'elemento <main> correlato richiede un aggiornamento dell'attributo aria-busy:

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

Cancella gli attributi se la quantità di caricamento è sconosciuta

Se il valore è sconosciuto o non impostato, null in questo utilizzo, rimuovi gli attributi value e aria-valuenow. In questo modo, il valore <progress> diventerà indeterminato.

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

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

Risolvere i problemi di matematica decimale in JavaScript

Poiché ho scelto di mantenere il valore massimo predefinito di 1 per l'avanzamento, le funzioni di incremento e decremento della demo utilizzano la matematica decimale. JavaScript e altri linguaggi non sono sempre molto bravi in questo. Ecco una funzione roundDecimals() che taglia l'eccesso dal risultato matematico:

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

Arrotonda il valore in modo che possa essere presentato e sia leggibile:

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

Impostare il valore per gli screen reader e lo stato del browser

Il valore viene utilizzato in tre posizioni nel DOM:

  1. L'attributo value dell'elemento <progress>.
  2. L'attributo aria-valuenow.
  3. Il contenuto del testo interno di <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
}

Dare l'attenzione all'avanzamento

Con i valori aggiornati, gli utenti vedenti vedranno la modifica dell'avanzamento, ma gli utenti degli screen reader non hanno ancora ricevuto l'annuncio della modifica. Metti in primo piano l'elemento<progress> e il browser annuncerà l'aggiornamento.

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 dell&#39;app Voice Over per Mac OS 
  che legge all&#39;utente l&#39;avanzamento della barra di caricamento.

Conclusione

Ora che sai come ho fatto, come faresti? 🙂

Se mi venisse data un'altra possibilità, vorrei apportare alcune modifiche. Penso che ci sia spazio per ripulire il componente corrente e per provare a crearne uno senza le limitazioni di stile pseudo-classe dell'elemento <progress>. Vale la pena esplorarlo.

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.

Crea una demo, twittami con i link e io la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community