Creazione di un componente della barra di caricamento

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

In questo post voglio condividere alcune riflessioni su come creare una barra di caricamento accessibile e adattabile ai colori con l'elemento <progress>. Prova la demo e visualizza l'origine.

Chiaro e scuro, indeterminato, crescente e completamento dimostrati 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 acustico sul completamento. Questo feedback visivo è utile per scenari come: avanzamento di un modulo, visualizzazione di informazioni di download o caricamento o persino indicazione che l'avanzamento è sconosciuto, ma l'attività è ancora in corso.

Questa sfida sulla GUI ha utilizzato l'elemento HTML <progress> esistente per risparmiare un po' di lavoro sull'accessibilità. I colori e i layout spingono i limiti della personalizzazione per l'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'icona adattiva dall'alto verso il basso: Safari, Firefox, Chrome.
Demo mostrata su Firefox, Safari, Safari per iOS, Chrome e Chrome per Android in schemi chiari e scuri.

Segni e linee

Ho scelto di inserire l'elemento <progress> in un <label> per 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 degli screen reader possano trasmettere queste informazioni a un utente.

<progress></progress>

Se non è presente value, l'avanzamento dell'elemento è indeterminato. L'attributo max è impostato su 1 per impostazione predefinita, quindi l'avanzamento è compreso tra 0 e 1. Ad esempio, se imposti max su 100, 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 in un'etichetta come questa:

<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 <span> e applica 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 di DevTools che mostra l'elemento screen-ready-only.

Area interessata dall'avanzamento del caricamento

Se hai una vista normale, può essere facile associare un indicatore di avanzamento agli elementi e alle aree della pagina correlati, ma per gli utenti con disabilità visiva non è così chiaro. Per migliorare questo aspetto, assegna 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, attiva 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 su uno stato sconosciuto, il che è più chiaro rispetto a osservare che l'elemento non ha un attributo 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 attivabile da JavaScript. Questo è importante per la tecnologia degli screen reader, poiché l'assegnazione dello stato attivo all'avanzamento man mano che cambia annuncerà all'utente fino a che punto è arrivato l'avanzamento aggiornato.

Stili

L'elemento progress è un po' complicato quando si tratta di applicare 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 dell'elemento di avanzamento e nella posizione dell'etichetta. Viene aggiunto uno stato di completamento speciale che può essere un indicatore visivo aggiuntivo utile, ma non obbligatorio.

Layout <progress>

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 tra 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 numerica scientifica per esprimere un numero elevato, pertanto border-radius viene sempre arrotondato. È equivalente a 1000px. Mi piace usarlo perché il mio obiettivo è utilizzare un valore abbastanza grande da poterlo impostare e dimenticare (ed è più breve da scrivere rispetto a 1000px). È anche facile ingrandirlo ulteriormente, se necessario: basta cambiare il 3 in 4, quindi 1e4px equivale a 10000px.

overflow: hidden è utilizzato ed è uno stile controverso. In questo modo, alcune cose sono state semplificate, ad esempio non è stato necessario passare i valori border-radius alla barra e agli elementi di riempimento della barra, ma ha anche significato che nessun elemento secondario dell'indicatore di avanzamento 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 svolgono il lavoro più difficile confrontando il valore massimo con il valore e, se corrispondono, l'avanzamento è completo. Al termine, viene generato uno pseudo-elemento che viene 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 utilizza i propri colori per l'elemento di avanzamento e si adatta alla modalità Luce e Buio con una sola proprietà CSS. Questo può essere ampliato con alcuni selettori specifici del browser.

Stili del browser chiaro e scuro

Per attivare un elemento <progress> adattivo scuro e chiaro per il 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 accent-color. Il browser garantisce un contrasto adeguato: è fantastico.

Colori chiari e scuri completamente personalizzati

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

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 selezione

In precedenza abbiamo assegnato all'elemento un indice di tabulazione negativo in modo che potesse essere impostato lo stato attivo in modo programmatico. Utilizza :focus-visible per personalizzare lo stato attivo e attivare lo stile dell'anello di attivazione più smart. In questo modo, un clic del mouse e lo stato attivo non mostreranno l'anello di selezione, ma i clic della tastiera sì. Il video di YouTube approfondisce questo argomento e vale la pena guardarlo.

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

Stili personalizzati nei browser

Personalizza gli stili selezionando le parti di un elemento <progress> esposte da ogni browser. L'utilizzo dell'elemento progress è un singolo tag, ma è composto da alcuni elementi secondari esposti tramite pseudo-selettori CSS. Se attivi l'impostazione, Chrome DevTools ti mostrerà questi 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 Impostazioni nell'angolo in alto a destra della finestra di DevTools.
  3. Sotto l'intestazione Elementi, individua e attiva la casella di controllo Mostra DOM shadow dello user agent.

Screenshot della posizione in DevTools in cui attivare l'esposizione del DOM shadow dello user agent.

Stili di 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 alla modalità Luce 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'elemento di avanzamento.

Stili di Firefox

Firefox espone solo lo pseudoselettore ::-moz-progress-bar 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'elemento di avanzamento.

Screenshot dell'angolo di debug in cui Safari, Safari per iOS, Firefox, Chrome e Chrome su Android mostrano la barra di caricamento in funzione.

Nota che Firefox ha un colore della barra impostato da accent-color, mentre Safari per iOS ha una barra azzurra. Lo stesso vale per la modalità Buio: Firefox ha una barra scura, ma non il colore personalizzato che abbiamo impostato, mentre funziona nei browser basati su Webkit.

Animazione

Quando si lavora con gli pseudo-selettori integrati del browser, spesso si utilizza un insieme limitato di proprietà CSS consentite.

Animazione della traccia che si riempie

L'aggiunta di una transizione alla pseudo-classe inline-size dell'elemento di avanzamento 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 posso essere un po' più creativo e 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 ottime per molte cose, ma una delle mie preferite è semplicemente dare un nome a un valore CSS che altrimenti sembrerebbe magico. Di seguito è riportata una linear-gradient abbastanza complessa, ma con un nome carino. Lo scopo e i 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.

I fotogrammi chiave

L'obiettivo è un'animazione infinita che va avanti e indietro. I keyframe di inizio e fine verranno impostati in CSS. È necessario un solo fotogramma chiave, quello centrale in 50%, per creare un'animazione che torna al punto di partenza, più e più volte.

@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 barra rispetto a uno pseudo-elemento, quindi eseguo l'upgrade dagli pseudo-elementi come base alle barre di animazione.

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 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 allo pseudo-elemento della barra di avanzamento:

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, che aggiornano state.val e poi chiamano una funzione per aggiornare il DOM.

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

setProgress()

In questa funzione avviene l'orchestrazione della 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 sulla zona <main>

A seconda che l'avanzamento sia completo o meno, l'elemento <main> correlato deve essere aggiornato con l'attributo aria-busy:

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

Cancella gli attributi se l'importo di ricarica è sconosciuto

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

Risolvi i problemi di calcolo 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 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 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. I contenuti di testo interni 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
}

Spostamento dello stato di avanzamento in primo piano

Con i valori aggiornati, gli utenti vedenti vedranno la modifica dello stato di avanzamento, ma gli utenti di screen reader non ricevono ancora l'annuncio della modifica. Sposta 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'app VoiceOver di macOS che legge all'utente l'avanzamento della barra di caricamento.

Conclusione

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

Ci sono sicuramente alcune modifiche che vorrei apportare se ne avessi l'opportunità. Penso che ci sia spazio per ripulire il componente attuale e per provare a crearne uno senza le limitazioni di stile delle pseudo-classi dell'elemento <progress>. Vale la pena approfondire.

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

Crea una demo, inviami i link su X e li aggiungerò alla sezione dei remix della community qui sotto.

Remix della community