Creazione di un componente della barra di caricamento

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

In questo post voglio condividere i miei pensieri su come creare una barra di caricamento adattiva e accessibile ai colori con l'elemento <progress>. Prova la demo e visualizza la fonte.

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.

Questa Sfida GUI ha funzionato con l'elemento <progress> HTML esistente per ridurre lo sforzo 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.

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

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 inoltrare tali informazioni a un utente.

<progress></progress>

Se non è presente value, l'avanzamento dell'elemento è indeterminato. Per impostazione predefinita, l'attributo max è 1, quindi l'avanzamento è compreso tra 0 e 1. Se imposti max su 100, ad esempio, l'intervallo verrà 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 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 gli screen reader. Per farlo, il testo dell'etichetta è riavvolto in un <span> e vengono applicati alcuni stili in modo che sia effettivamente fuori schermo:

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

Con il seguente CSS associato 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 di devtools che mostra l&#39;elemento &quot;screen ready only&quot;.

Area interessata dall'avanzamento del caricamento

Se hai una vista sana, può essere facile associare un indicatore di avanzamento agli elementi e alle aree di pagina correlati, ma per gli utenti con disabilità visiva il concetto non è così chiaro. Migliora questo aspetto 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, passa da aria-busy a true all'inizio dell'attività e a false al termine dell'attività.

Aggiunte attributi Aria

Anche se il ruolo implicito di un elemento <progress> è progressbar, l'ho reso esplicito per i browser che non lo hanno. Ho anche aggiunto l'attributo indeterminate per impostare esplicitamente l'elemento in uno stato sconosciuto, in modo che sia più chiaro rispetto all'assenza di value impostato per l'elemento.

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

Utilizza tabindex="-1" per rendere attivabile l'elemento di avanzamento 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 delle etichette dell'elemento di avanzamento. Viene aggiunto uno stato di completamento speciale che può rappresentare un segnale visivo aggiuntivo utile, ma non obbligatorio.

Layout <progress>

La larghezza dell'elemento di avanzamento non viene modificata, quindi può restringersi e aumentare con lo 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 1e3px di _radius utilizza la notazione scientifica dei numeri per esprimere un numero elevato, in modo che border-radius venga sempre arrotondato. Equivale a 1000px. Mi piace usare questo dato perché il mio obiettivo è utilizzare un valore abbastanza grande da poter impostare e dimenticare (ed è più breve da scrivere rispetto a 1000px). Se necessario, è anche facile renderlo ancora più grande: basta cambiare il 3 in 4, quindi 1e4px è equivalente a 10000px.

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

Operazione completata

I selettori CSS fanno il resto, confrontando il valore massimo con il valore. Se corrispondono, l'avanzamento è completo. Al termine, viene generato uno pseudo-elemento che viene aggiunto alla fine dell'elemento avanzamento, offrendo 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 utilizza i propri colori per l'elemento Progressi e si adatta al lucido chiaro e allo scuro con una sola proprietà CSS. Puoi basarti su alcuni selettori speciali per i browser.

Stili del browser chiaro e scuro

Per attivare per il tuo sito un elemento <progress> adattivo chiaro e scuro, color-scheme è tutto ciò che serve.

progress {
  color-scheme: light dark;
}

Colore colore avanzamento singola proprietà

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

progress {
  accent-color: rebeccapurple;
}

Nota che il colore di sfondo della traccia passa da chiaro a scuro in base al accent-color. Il browser sta garantendo il giusto contrasto: abbastanza chiaro.

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. Nella query multimediale prefers-color-scheme, fornisci nuovi valori di colore per la traccia 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%);
  }
}

Imposta lo stato attivo sugli stili

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 lo stato attivo non mostreranno l'anello di messa a fuoco, mentre i clic della tastiera verranno visualizzati. Il video di YouTube approfondisce l'argomento e vale la pena di 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 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 l'intestazione Elementi, trova e attiva la casella di controllo Mostra DOM shadow dello user agent.

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

Stili di 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 al chiaro e al buio.

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

Stili di Firefox

Firefox espone solo lo pseudoselettore ::-moz-progress-bar sull'elemento <progress>. Ciò significa anche che non possiamo applicare direttamente la tinta alla traccia.

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

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

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

Nota che per Firefox è stato impostato un colore delle tracce accent-color, mentre per iOS Safari è impostato un colore per le tracce azzurro. Lo stesso avviene nella 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 lavora con pseudo selettori integrati nel browser, spesso ha 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 su ::-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 uno pseudo-elemento per Chromium e viene applicato un gradiente che viene animato in avanti e indietro per tutti e tre i browser.

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 è riportata una descrizione di linear-gradient piuttosto complessa, 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 consentono inoltre di mantenere il codice 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 di inizio e fine saranno impostati in CSS. È necessario un solo fotogramma chiave, quello centrale in 50%, per creare un'animazione che ritorni al punto di partenza, ripetutamente.

@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> o consentono di animare la barra di avanzamento. Sempre più browser supportano l'animazione della traccia rispetto a uno pseudo-elemento, quindi eseguo l'upgrade dagli pseudo-elementi come base a barre animate.

Pseudo-elemento Chromium

Chromium consente lo pseudo-elemento ::after utilizzato con una posizione per coprire l'elemento. Vengono utilizzate le proprietà personalizzate indeterminate e l'animazione indietro e avanti 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 degli 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

In Firefox, le proprietà personalizzate e un'animazione vengono applicate anche alla barra di avanzamento degli 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 quindi chiamano una funzione per aggiornare il DOM.

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

setProgress()

Questa funzione è il punto in cui 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 per l'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 viene configurato, null in questo utilizzo, rimuovi gli attributi value e aria-valuenow. In questo modo, l'elemento <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 predefinito massimo di 1 per l'avanzamento, le funzioni di incremento e decremento della demo utilizzano la matematica decimale. JavaScript e altri linguaggi non sono sempre ottimi 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 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. Attributo aria-valuenow.
  3. I contenuti 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 sul progresso

Con i valori aggiornati, gli utenti vedenti vedranno il cambiamento nell'avanzamento, ma agli utenti di screen reader non è ancora stato comunicato il cambiamento. 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 ho fatto, come faresti‽ 🙂

Di sicuro vorrei apportare alcune modifiche se ci venisse offerta un'altra possibilità. 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 esplorare!

Diversificaamo 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