Creazione di un componente della barra di caricamento

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

In questo post vorrei condividere pensando a come creare una barra di caricamento colore adattiva e accessibile con l'elemento <progress>. Prova la demo e visualizza la fonte.

Chiaro e scuro, indeterminato, in aumento e completamento dimostrati su Chrome.

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

Panoramica

L'elemento <progress> fornisce agli utenti feedback visivi e udibili sul completamento. Questo feedback visivo è utile per scenari quali l'avanzamento di un modulo, la visualizzazione di informazioni relative al download o al caricamento o persino la segnalazione che l'importo dell'avanzamento è sconosciuto ma il lavoro è ancora attivo.

Questa GUI Challenge ha funzionato con l'elemento HTML <progress> esistente per risparmiare un po' di impegno in termini di accessibilità. I colori e i layout superano i limiti della personalizzazione per l'elemento integrato, per modernizzare il componente e adattarlo meglio ai sistemi di progettazione.

Le schede chiare e scure di ogni browser forniscono una panoramica dell&#39;icona adattiva dall&#39;alto verso il basso: Safari, Firefox, Chrome.
Demo mostrata su Firefox, Safari, Safari, iOS, Chrome e Chrome per Android in schemi chiari e scuri.

Markup

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. Per impostazione predefinita, l'attributo max è pari a 1, quindi l'avanzamento è compreso tra 0 e 1. Se, ad esempio, imposti max su 100, l'intervallo viene impostato su 0-100. Ho scelto di rimanere entro i limiti 0 e 1, traducendo i valori dei progressi in 0,5 o 50%.

Avanzamento con wrapping delle etichette

In una relazione implicita, un elemento di avanzamento viene racchiuso in un'etichetta come la seguente:

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

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

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

Con il seguente CSS di WebAIM associato:

.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 di DevTools che mostra l&#39;elemento &quot;Solo schermo pronto&quot;.

Area interessata dall'avanzamento del caricamento

Se hai una visione sana, può essere facile associare un indicatore di avanzamento a elementi e aree della pagina correlati, ma per gli utenti con disabilità visiva non è molto chiaro. Migliora questa funzionalità assegnando l'attributo aria-busy all'elemento più in alto che cambierà al termine del caricamento. Inoltre, indica una relazione 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, imposta aria-busy su true all'inizio dell'attività e su false una volta terminata.

Aggiunte attributi Aria

Anche se il ruolo implicito di un elemento <progress> è progressbar, l'ho reso esplicito per i browser a cui manca questo ruolo implicito. Ho anche aggiunto l'attributo indeterminate per mettere esplicitamente l'elemento in uno stato di sconosciuto, 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 attivare l'elemento avanzamento da JavaScript. Questo è importante per la tecnologia di screen reader, poiché l'attenzione posta sui progressi a mano a mano che cambia, annuncerà all'utente quanto sono stati raggiunti i progressi aggiornati.

Stili

L'elemento progresso è un po' difficile quando si tratta di stili. Gli elementi HTML integrati hanno parti speciali nascoste che possono essere difficili da selezionare e spesso offrono solo un insieme limitato di proprietà da impostare.

Layout

Gli stili di layout sono progettati per consentire una certa flessibilità in termini di dimensioni e posizione dell'etichetta dell'elemento di avanzamento. Viene aggiunto uno stato di completamento speciale, che può essere un segnale visivo utile, ma non obbligatorio.

Layout <progress>

La larghezza dell'elemento di avanzamento non viene modificata in modo che possa ridursi e aumentare lo spazio necessario nel design. Gli stili integrati vengono rimossi impostando appearance e border su none. Questo avviene in modo che l'elemento possa essere normalizzato tra i vari 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 1e3px per _radius utilizza la notazione scientifica per esprimere un numero grande in modo che border-radius venga sempre arrotondato. Equivale a 1000px. Mi piace utilizzarla perché il mio obiettivo è utilizzare un valore abbastanza grande da impostarlo e dimenticarlo (ed è più breve da scrivere rispetto a 1000px). È anche semplice renderlo ancora più grande, se necessario: basta cambiare il 3 in 4, quindi 1e4px è equivalente a 10000px.

È stato utilizzato uno stile controverso overflow: hidden. Ciò semplificava le operazioni, ad esempio non era necessario trasferire i valori border-radius alla traccia e tenere traccia degli elementi di riempimento; tuttavia, significava anche che gli elementi secondari dell'avanzamento non potevano trovarsi al di fuori dell'elemento. Un'altra iterazione su questo elemento di avanzamento personalizzato potrebbe essere eseguita senza overflow: hidden e potrebbe aprire alcune opportunità per le animazioni o gli stati di completamento migliori.

Operazione completata

In questo caso i selettori CSS svolgono la maggior parte del lavoro difficile confrontando il valore massimo con il valore e, in caso di corrispondenza, il progresso è completo. Al termine, viene generato uno pseudo-elemento e aggiunto alla fine dell'elemento progresso, che fornisce un ulteriore segnale visivo per il 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 che mostra un segno di spunta alla fine.

Colore

Il browser applica i propri colori per l'elemento di avanzamento e si adatta al chiaro e allo scuro con una sola proprietà CSS. Questi selettori possono essere creati con alcuni selettori specifici del browser.

Stili del browser chiaro e scuro

Per attivare l'elemento <progress> adattivo chiaro e scuro per il tuo sito, color-scheme è sufficiente.

progress {
  color-scheme: light dark;
}

Colore di riempimento dell'avanzamento della singola proprietà

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

progress {
  accent-color: rebeccapurple;
}

Osserva che il colore di sfondo della traccia cambia da chiaro a scuro a seconda del accent-color. Il browser sta garantendo il contrasto adeguato: piuttosto ordinato.

Colori chiari e scuri completamente personalizzati

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

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 per consentirgli di concentrarsi in modo programmatico. Usa :focus-visible per personalizzare la messa a fuoco e attivare lo stile dell'anello di messa a fuoco più intelligente. In questo modo, il clic del mouse e lo stato attivo non mostreranno l'anello di messa a fuoco, mentre i clic della tastiera sì. Il video di YouTube approfondisce ulteriormente questo aspetto e vale la pena esaminarlo.

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

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

Stili personalizzati nei vari browser

Personalizza gli stili selezionando le parti di un elemento <progress> esposte da ciascun browser. L'utilizzo dell'elemento progresso è un singolo tag, ma è costituito da alcuni elementi secondari esposti tramite gli pseudo selettori CSS. Se attivi l'impostazione, Chrome DevTools ti mostrerà i seguenti elementi:

  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 l'intestazione Elementi, trova e attiva la casella di controllo Mostra DOM shadow dello user agent.

Screenshot della posizione in DevTools per attivare l&#39;esposizione dello shadow DOM 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 l'utilizzo di un sottoinsieme di CSS. Per ora, imposta background-color utilizzando le proprietà personalizzate create in precedenza, che si adattano alla luce e allo scuro.

/*  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 progresso.

Stili Firefox

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

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

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

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

Tieni presente che il colore della traccia di Firefox è impostato su accent-color, mentre iOS Safari ha una traccia azzurro. Si tratta della stessa cosa con la modalità Buio: Firefox ha una traccia scura, ma non il colore personalizzato che abbiamo impostato. Inoltre, funziona nei browser basati su Webkit.

Animazione

Quando si utilizzano gli pseudo selettori integrati nel browser, spesso si tratta di un insieme limitato di proprietà CSS consentite.

Animazione del riempimento della traccia

L'aggiunta di una transizione a inline-size dell'elemento progresso funziona per Chromium, ma non per Safari. Inoltre, Firefox non utilizza una proprietà di transizione sul 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 ho un po' più di creatività, in modo da poter fornire un'animazione. Viene creato uno pseudo-elemento per Chromium e viene applicato un gradiente che viene animato avanti e indietro per tutti e tre i browser.

Le proprietà personalizzate

Le proprietà personalizzate sono ideali per molti aspetti, ma una delle mie preferite è semplicemente dare un nome a un valore CSS dall'aspetto magico. La seguente è una linear-gradient abbastanza complessa, ma con un bel nome. Lo scopo e i casi d'uso devono essere comprensibili in modo chiaro.

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

Inoltre, consentono di mantenere il codice DRY (DRY) poiché, ancora una volta, non possiamo raggruppare questi selettori specifici del browser.

I fotogrammi chiave

L'obiettivo è un'animazione infinita che va avanti e indietro. I fotogrammi chiave iniziale e finale verranno impostati in CSS. È sufficiente un solo fotogramma chiave, ovvero il fotogramma chiave centrale in 50%, per creare un'animazione che torni al punto da cui è iniziato, più e più volte.

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

Targeting di ciascun browser

Non tutti i browser consentono la creazione di pseudo-elementi sull'elemento <progress> stesso o consente di animare la barra di avanzamento. Più browser supportano l'animazione della traccia rispetto a uno pseudo-elemento, quindi eseguo l'upgrade da pseudo-elementi come base e a barre animate.

Pseudo elemento Chromium

Chromium consente invece lo pseudo-elemento ::after utilizzato 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

In Safari, le proprietà personalizzate e un'animazione vengono applicate alla barra di avanzamento dello 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 dello pseudo elemento:

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 richiamano una funzione per l'aggiornamento del DOM.

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

setProgress()

È qui che avviene l'orchestrazione 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 deve essere aggiornato all'attributo aria-busy:

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

Cancella attributi se la quantità di caricamento non è nota

Se il valore è sconosciuto o non viene impostato, null in questo utilizzo, rimuovi gli attributi value e aria-valuenow. <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 in formato decimale JavaScript

Poiché ho scelto di rispettare il valore predefinito di avanzamento massimo di 1, le funzioni di incremento e decremento della demo utilizzano calcoli matematici decimali. JavaScript e altri linguaggi non sempre sono ottimi in questo. Ecco una funzione roundDecimals() che ridurrà 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 reso 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 + "%"
}

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

Il valore viene utilizzato in tre posizioni nel DOM:

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

Concentrarsi sui progressi

Con i valori aggiornati, gli utenti vedenti vedranno il cambiamento, ma gli utenti di screen reader non hanno ancora ricevuto l'annuncio della modifica. Imposta lo stato attivo sull'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 di Mac OS 
  che legge l&#39;avanzamento della barra di caricamento per l&#39;utente.

Conclusione

Ora che sai come ci sono riuscito, come faresti? 🙂

Di sicuro ci sono alcune modifiche che vorrei apportare se ci fosse un'altra possibilità. Penso che ci sia spazio per ripulire il componente attuale e spazio per provare a crearne uno senza i limiti di stile delle pseudo-classi dell'elemento <progress>. Vale la pena di esplorarla.

Diversifica i nostri approcci e scopriamo tutti i modi per creare sul web.

Crea una demo, inviami un tweet con i link e lo aggiungerò alla sezione Remix della community di seguito.

Remix della community