Creazione di un componente della barra di caricamento

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

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

Demo di luce e buio, stato indeterminato, aumento e 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 in scenari come: avanzamento di un modulo, visualizzazione di informazioni sul download o sul caricamento o anche per mostrare che la quantità di avanzamento è sconosciuta, ma il lavoro è ancora attivo.

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

Schede chiare e scure in ogni browser che forniscono una panoramica dell&#39;icona adattiva dall&#39;alto verso il basso: Safari, Firefox, Chrome.
Demo 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 di relazione espliciti a favore di una relazione implicita. Ho anche etichettato un elemento principale interessato dallo stato di caricamento, in modo che le tecnologie di lettura dello schermo possano trasmettere queste informazioni a un utente.

<progress></progress>

Se non è presente value, lo stato di avanzamento dell'elemento è indeterminato. L'attributo max ha come valore predefinito 1, quindi l'avanzamento è compreso tra 0 e 1. Se imposti max su 100, ad esempio, l'intervallo sarà 0-100. Ho scelto di rimanere entro i limiti 0 e 1, traducendo i valori di avanzamento in 0,5 o 50%.

Avanzamento con etichetta

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

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

Nella mia demo ho scelto di includere l'etichetta solo per gli screen reader. A questo scopo, il testo dell'etichetta viene racchiuso in un <span> e vengono applicati alcuni stili in modo che sia effettivamente fuori dallo schermo:

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

Con il seguente CSS di accompagnamento 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 di sviluppo che mostrano l&#39;elemento di sola lettura dello schermo.

Area interessata dal caricamento in corso

Se hai una vista normale, può essere facile associare un indicatore di avanzamento a elementi e aree della pagina correlati, ma per gli utenti con disabilità visive 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, imposta aria-busy su true all'inizio dell'attività e su false al termine.

Aggiunte di attributi ARIA

Anche se il ruolo implicito di un elemento <progress> è progressbar, l'ho reso esplicito per i browser che non hanno questo ruolo implicito. Ho anche aggiunto l'attributo indeterminate per impostare esplicitamente l'elemento in uno stato 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 rendere l'elemento di avanzamento selezionabile da JavaScript. Questo è importante per la tecnologia di lettura dello schermo, poiché, dando il focus di avanzamento man mano che l'avanzamento cambia, l'utente viene informato di 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 sono pensati per 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 suggerimento visivo aggiuntivo utile, ma non obbligatorio.

<progress> Layout

La larghezza dell'elemento di avanzamento rimane 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é ognuno 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 per esprimere un numero elevato, quindi border-radius viene sempre arrotondato. È equivalente a 1000px. Mi piace usare questo valore perché il mio obiettivo è utilizzarne uno abbastanza grande da poterlo impostare e dimenticare (ed è più breve da scrivere di 1000px). Inoltre, è facile renderlo ancora più grande se necessario: basta cambiare il 3 in 4, quindi 1e4px equivale a 10000px.

overflow: hidden è utilizzato ed è stato uno stile controverso. Ha semplificato alcune cose, ad esempio non è necessario passare i valori border-radius alla traccia e agli elementi di riempimento della traccia, ma ha anche impedito che gli elementi secondari della barra di avanzamento potessero 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 è completato. Al termine, viene generato uno pseudo-elemento e aggiunto alla fine dell'elemento di avanzamento, fornendo un ulteriore segnale 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% con un segno di spunta alla fine.

Colore

Il browser porta i propri colori per l'elemento di avanzamento ed è adattabile alla modalità Chiaro e Buio con una sola proprietà CSS. Questi possono essere integrati con alcuni selettori speciali specifici del browser.

Stili del browser chiaro e scuro

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

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 come il colore di sfondo della traccia cambia da chiaro a scuro a seconda del accent-color. Il browser garantisce un contrasto adeguato: niente male.

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 multimediale 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 messo a fuoco 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, un clic del mouse e lo stato attivo non mostreranno l'anello di evidenziazione, ma i clic della tastiera sì. Il video di YouTube approfondisce l'argomento e vale la pena di guardarlo.

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

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

Stili personalizzati su più browser

Personalizza gli stili selezionando le parti di un elemento <progress> che ogni browser espone. L'utilizzo dell'elemento di avanzamento è un singolo tag, ma è composto da alcuni elementi secondari esposti tramite pseudo selettori CSS. Chrome DevTools 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 delle impostazioni a forma di ingranaggio nell'angolo in alto a destra della finestra DevTools.
  3. Nella sezione Elementi, individua e seleziona la casella di controllo Mostra DOM shadow dell'agente utente.

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

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 ora, imposta background-color utilizzando le proprietà personalizzate create in precedenza, che si adattano alla modalità Chiaro e 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 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 di dove trovare le parti dell&#39;elemento di avanzamento.

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

Nota che Firefox ha un colore della traccia impostato da accent-color, mentre Safari su iOS ha una traccia azzurra. Lo stesso vale per la 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 gli pseudo-selettori integrati nel browser, spesso si utilizza 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. Firefox inoltre 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;
}

Animare lo stato :indeterminate

Qui divento un po' più creativo, così posso 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 utili per molte cose, ma una delle mie preferite è semplicemente dare un nome a un valore CSS altrimenti magico. Di seguito è riportato un esempio piuttosto linear-gradient, ma con un nome carino. Il suo scopo e i suoi casi d'uso possono 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 aiuteranno anche a mantenere il codice DRY, poiché ancora una volta non possiamo raggruppare questi selettori specifici del browser.

Fotogrammi chiave

L'obiettivo è un'animazione infinita che va avanti e indietro. I fotogrammi chiave iniziali e finali verranno impostati in CSS. Per creare un'animazione che torna al punto di partenza, ancora e ancora, è necessario un solo fotogramma chiave, quello centrale a 50%.

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

Targeting di ogni browser

Non tutti i browser consentono la creazione di pseudo-elementi sull'elemento <progress> stesso 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 e passo alle barre animate.

Pseudo-elemento Chromium

Chromium consente l'utilizzo dello pseudo-elemento: ::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 pseudo-barra di avanzamento dell'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; questi aggiornano state.val e poi 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 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 i progressi siano completi o meno, l'elemento <main> correlato deve essere aggiornato all'attributo aria-busy:

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

Cancella gli attributi se l'importo del caricamento è sconosciuto

Se il valore è sconosciuto o non impostato, null in questo utilizzo, rimuovi gli attributi value e aria-valuenow. In questo modo, <progress> verrà impostato su 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 adatti a questo scopo. Ecco una funzione roundDecimals() che taglierà l'eccesso dal risultato del calcolo:

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

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

Focus sull'avanzamento

Con i valori aggiornati, gli utenti vedranno il cambiamento dello stato di avanzamento, ma gli utenti di screen reader non riceveranno ancora l'annuncio della modifica. Concentrati 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 VoiceOver di Mac OS 
  che legge all&#39;utente l&#39;avanzamento della barra di caricamento.

Conclusione

Ora che sai come ho fatto, come faresti tu?‽ 🙂

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

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

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

Remix della community