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.

Illustrazione di sfondo chiaro e scuro, indeterminate, in aumento e di completamento 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 è utile per scenari come: avanzamento in un modulo, visualizzazione di informazioni di download o caricamento o anche per mostrare che l'importo dell'avanzamento è sconosciuto, 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 in ogni browser che forniscono una panoramica dell&#39;icona adattiva da cima a fondo: 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 racchiudere l'elemento <progress> in un <label> in modo da poter saltare gli attributi della relazione esplicita in 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%.

Aggiornamento con etichetta inserita nel codice

In una relazione implicita, un elemento di avanzamento è racchiuso in un'etichetta come questa:

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

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

Aggiunta di attributi Aria

Anche se il ruolo implicito di un elemento <progress> è 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é l'assegnazione dell'attenzione all'avanzamento man mano che cambia, consente di comunicare all'utente il livello di 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 uniformato su tutti i browser, poiché ogni browser ha i propri stili per l'elemento.

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 grande da poterlo impostare e dimenticare (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 è stato necessario passare i valori border-radius al tracciato e agli elementi di riempimento del tracciato, ma ha anche significato che nessun elemento secondario del progresso poteva trovarsi 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 utile.

Colori chiari e scuri completamente personalizzati

Imposta due proprietà personalizzate sull'elemento <progress>, una per il colore del canale e l'altra per il colore dell'avanzamento del canale. 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 selezionato tramite codice. Utilizza :focus-visible per personalizzare lo stato attivo e attivare lo stile dell'anello di messa a fuoco più intelligente. In questo modo, un clic del mouse e l'evidenziazione dell'elemento attivo non mostreranno l'anello di evidenziazione, 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 un anello di messa a fuoco intorno. 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 di avanzamento è un singolo tag, ma è costituito da alcuni elementi secondari esposti tramite gli pseudo-selettori CSS. Se attivi l'impostazione, Chrome DevTools mostrerà questi elementi:

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

Screenshot della posizione in DevTools in cui attivare l&#39;esposizione del DOM shadow dello user agent.

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 della sezione Debug in cui Safari, Safari per iOS, Firefox, Chrome e Chrome su Android mostrano la barra di caricamento in funzione.

Tieni presente che Firefox ha un colore della 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 al 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 essere un po' più creativo e fornire un'animazione. Viene creato uno 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 molte cose, ma una delle mie preferite è semplicemente dare 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 sono chiari.

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'obiettivo è un'animazione infinita che va avanti e indietro. I fotogrammi chiave di inizio e fine verranno impostati in CSS. Per creare un'animazione che ritorna all'inizio, più e più volte, è necessario un solo fotogramma chiave, quello intermedio in 50%.

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

Targeting per ciascun 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'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 poi richiamano 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 funzione setProgress(). 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)
}

Cancellare 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 di 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 elimina le parti in 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 riorganizzare il componente attuale e per provare a crearne uno senza le limitazioni dello stile delle pseudoclassi dell'elemento <progress>. Vale la pena esplorarlo.

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

Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.

Remix della community