Creare un loader con i CSS senza compromettere l’accessibilità

I loader sono utili, ma se progettati male possono creare problemi: annunciano informazioni inutili agli screen reader, muovono troppo l’interfaccia e sfidano il contrasto o la percezione. Qui trovi linee guida pratiche e snippet pronti all’uso per un loader CSS accessibile, con attenzione a ARIA, movimento ridotto e alternative determinate.

Principi chiave

  • Non bloccare la lettura dello screen reader: usa role="status" o elementi nativi (<progress>), evita annunci ripetitivi.
  • Rispetta chi riduce le animazioni: onora prefers-reduced-motion.
  • Offri alternative determinate quando possibile: una barra di avanzamento è meglio di un’animazione infinita.
  • Non affidarti solo al colore/forma: aggiungi testo nascosto “Caricamento…”.
  • Gestisci il focus: non spostarlo sul loader; resta dove serve all’utente.

Spinner minimale (indeterminato) e accessibile

Questo spinner è silenzioso per gli screen reader (annuncia una sola volta lo stato), riduce l’animazione se necessario e usa solo <span>.

<!-- Markup -->
<span class="loader" role="status" aria-live="polite" aria-busy="true">
  <span class="sr-only">Caricamento…</span>
</span>
/* CSS base */
.loader {
  display: inline-block;
  width: 1.25rem;
  height: 1.25rem;
  vertical-align: text-bottom;
  position: relative;
}

.loader::before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  inset: 0;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* Testo solo per screen reader */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0);
  white-space: nowrap; border: 0;
}

/* Riduzione movimento */
@media (prefers-reduced-motion: reduce) {
  .loader::before {
    animation: none;
    border-right-color: currentColor; /* cerchio statico */
    opacity: 0.65;
  }
}

/* Animazione */
@keyframes spin {
  to { transform: rotate(360deg); }
}

Quando usarlo: attività brevi (<3–5 s) senza percentuale nota. Se il caricamento supera pochi secondi, passa a una soluzione determinata o a un testo di stato più informativo.

Alternativa determinata: barra nativa con <progress>

Quando conosci l’avanzamento, il componente nativo è la scelta più accessibile: espone ruolo e valori agli AT senza ARIA extra.

<label id="dl-label">Download in corso</label>
<progress value="42" max="100" aria-describedby="dl-label">42%</progress>
progress {
  width: 100%;
  height: 0.75rem;
}

/* Stile personalizzato (fallback generico) */
progress::-webkit-progress-bar { background: #eee; }
progress::-webkit-progress-value { background: currentColor; }
progress::-moz-progress-bar { background: currentColor; }

Nota: aggiorna value via JavaScript durante l’operazione. Il testo interno (es. “42%”) funge da fallback se lo stile nativo non è supportato.

Skeleton screen (per contenuti lunghi)

Lo skeleton riduce l’ansia da attesa mostrando l’ossatura del layout, evitando animazioni complesse. Mantienilo sobrio e non fuorviante.

<h3>Articoli consigliati</h3>
<p role="status" aria-busy="true">Caricamento contenuto…</p>

<p class="skeleton" aria-hidden="true">&nbsp;</p>
<p class="skeleton" aria-hidden="true">&nbsp;</p>
<p class="skeleton" aria-hidden="true">&nbsp;</p>
.skeleton {
  display: block;
  height: 1rem;
  margin: 0.5rem 0;
  border-radius: 0.25rem;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 37%, #eee 63%);
  background-size: 400% 100%;
  animation: shimmer 1.2s ease-in-out infinite;
}

@keyframes shimmer {
  0% { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
}

Aggiornare lo stato senza spam

Gli screen reader annunciano i cambiamenti in regioni live. Mantieni aria-live="polite" e aggiorna il testo in modo parsimonioso.

const status = document.querySelector('[role="status"] .sr-only');
function setStatus(message) {
  if (!status) return;
  status.textContent = message;
}
// Esempio: setStatus("Quasi pronto…");

Colori e dimensioni

  • Il loader deve rispettare il contrasto sullo sfondo (consigliato almeno 3:1 per elementi non testuali sottili).
  • Scala il componente in base al contesto: 16–24px per testo inline; 32–48px per container più grandi.

Checklist rapida

  • Il loader è ignorabile? Nessun focus forzato, niente annunci continui.
  • Rispetta prefers-reduced-motion?
  • Mostra testo di stato (anche nascosto) comprensibile?
  • Usa una barra determinata quando la progressione è nota?
  • Il contrasto è sufficiente nello stato attivo e inattivo?

Esempio completo (inline spinner, fallback ridotto)

<span class="loader" role="status" aria-live="polite" aria-busy="true">
  <span class="sr-only">Caricamento…</span>
</span>
<!-- Inserisci gli stili dello spinner e della classe .sr-only visti sopra -->

Con questi accorgimenti, avrai loader eleganti che non intralciano gli ausili, rispettano le preferenze di movimento e comunicano lo stato in modo chiaro.

Torna su