Créer un composant de barre de chargement

Présentation de base de la création d'une barre de chargement accessible et adaptative aux couleurs avec l'élément <progress>.

Dans cet article, je souhaite partager mes réflexions sur la création d'une barre de chargement accessible et adaptative aux couleurs avec l'élément <progress>. Essayez la démo et consultez la source.

Démonstration des thèmes clair et sombre, de l'état indéterminé, de l'augmentation et de la finalisation sur Chrome.

Si vous préférez les vidéos, voici une version YouTube de ce post :

Présentation

L'élément <progress> fournit aux utilisateurs un retour visuel et sonore sur l'achèvement. Ce retour visuel est utile dans des scénarios tels que la progression dans un formulaire, l'affichage d'informations de téléchargement ou d'importation, ou même l'indication que la progression est inconnue, mais que le travail est toujours actif.

Ce défi d'IUG a utilisé l'élément HTML <progress> existant pour faciliter l'accessibilité. Les couleurs et les mises en page repoussent les limites de la personnalisation de l'élément intégré, afin de moderniser le composant et de mieux l'intégrer aux systèmes de conception.

Onglets clairs et sombres dans chaque navigateur, offrant un aperçu de l'icône adaptative de haut en bas : Safari, Firefox, Chrome.
Démonstration sur Firefox, Safari, iOS Safari, Chrome et Android Chrome en thèmes clair et sombre.

Annoter

J'ai choisi d'encapsuler l'élément <progress> dans un <label> afin de pouvoir ignorer les attributs de relation explicites au profit d'une relation implicite. J'ai également étiqueté un élément parent concerné par l'état de chargement, afin que les technologies de lecteur d'écran puissent transmettre ces informations à l'utilisateur.

<progress></progress>

S'il n'y a pas de value, la progression de l'élément est indéterminée. L'attribut max est défini par défaut sur 1. La progression est donc comprise entre 0 et 1. Par exemple, si vous définissez max sur 100, la plage sera définie sur 0-100. J'ai choisi de rester dans les limites de 0 et 1, en traduisant les valeurs de progression par 0,5 ou 50 %.

Progression de l'étiquetage

Dans une relation implicite, un élément de progression est encapsulé par un libellé comme celui-ci :

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

Dans ma démonstration, j'ai choisi d'inclure le libellé pour les lecteurs d'écran uniquement. Pour ce faire, encapsulez le texte du libellé dans un <span> et appliquez-lui des styles afin qu'il soit effectivement hors de l'écran :

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

Avec le CSS suivant de 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;
}

Capture d'écran des outils de développement révélant l'élément réservé aux lecteurs d'écran.

Zone affectée par la progression du chargement

Si vous avez une bonne vue, il peut être facile d'associer un indicateur de progression aux éléments et zones de page associés, mais pour les utilisateurs malvoyants, ce n'est pas si évident. Pour améliorer ce point, attribuez l'attribut aria-busy à l'élément le plus haut qui changera une fois le chargement terminé. Indiquez également une relation entre la progression et la zone de chargement avec aria-describedby.

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

Dans JavaScript, définissez aria-busy sur true au début de la tâche, puis sur false une fois la tâche terminée.

Ajouts d'attributs ARIA

Bien que le rôle implicite d'un élément <progress> soit progressbar, je l'ai rendu explicite pour les navigateurs qui n'ont pas ce rôle implicite. J'ai également ajouté l'attribut indeterminate pour placer explicitement l'élément dans un état inconnu, ce qui est plus clair que d'observer que l'élément n'a pas d'attribut value défini.

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

Utilisez tabindex="-1" pour rendre l'élément de progression sélectionnable à partir de JavaScript. Ceci est important pour la technologie de lecteur d'écran, car le fait de donner le focus à la progression au fur et à mesure qu'elle change permet d'annoncer à l'utilisateur l'état d'avancement de la progression mise à jour.

Styles

L'élément de progression est un peu difficile à styliser. Les éléments HTML intégrés comportent des parties masquées spéciales qui peuvent être difficiles à sélectionner et ne proposent souvent qu'un ensemble limité de propriétés à définir.

Disposition

Les styles de mise en page sont destinés à offrir une certaine flexibilité dans la taille de l'élément de progression et la position du libellé. Un état d'achèvement spécial est ajouté, ce qui peut être un repère visuel supplémentaire utile, mais pas obligatoire.

Mise en page <progress>

La largeur de l'élément de progression n'est pas modifiée, de sorte qu'il peut se réduire et s'agrandir en fonction de l'espace nécessaire dans la conception. Les styles intégrés sont supprimés en définissant appearance et border sur none. Cela permet de normaliser l'élément sur tous les navigateurs, car chacun d'eux possède ses propres styles pour son élément.

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

La valeur de 1e3px pour _radius utilise la notation scientifique pour exprimer un grand nombre, de sorte que border-radius est toujours arrondi. Cela revient à 1000px. J'aime utiliser cette valeur, car mon objectif est d'utiliser une valeur suffisamment élevée pour pouvoir la définir et l'oublier (et elle est plus courte à écrire que 1000px). Il est également facile de l'augmenter encore si nécessaire : il suffit de remplacer le 3 par un 4, puis 1e4px équivaut à 10000px.

overflow: hidden est utilisé et a été un style controversé. Cela a facilité certaines choses, comme le fait de ne pas avoir à transmettre les valeurs border-radius à la piste et aux éléments de remplissage de la piste. Toutefois, cela signifiait également qu'aucun enfant de la progression ne pouvait se trouver en dehors de l'élément. Une autre itération de cet élément de progression personnalisé pourrait être effectuée sans overflow: hidden, ce qui pourrait ouvrir des possibilités d'animations ou de meilleurs états d'achèvement.

Processus terminé

Les sélecteurs CSS font le plus dur ici en comparant la valeur maximale avec la valeur actuelle. Si elles correspondent, la progression est terminée. Une fois l'opération terminée, un pseudo-élément est généré et ajouté à la fin de l'élément de progression, ce qui fournit un repère visuel supplémentaire agréable pour la fin de l'opération.

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

Capture d'écran de la barre de chargement à 100 % avec une coche à la fin.

Couleur

Le navigateur apporte ses propres couleurs pour l'élément de progression et s'adapte aux thèmes clair et sombre avec une seule propriété CSS. Vous pouvez vous appuyer sur ces éléments avec des sélecteurs spécifiques à certains navigateurs.

Styles de navigateur clair et sombre

Pour activer un élément <progress> adaptatif sombre et clair sur votre site, il vous suffit d'utiliser color-scheme.

progress {
  color-scheme: light dark;
}

Couleur de remplissage de la progression d'une seule propriété

Pour teinter un élément <progress>, utilisez accent-color.

progress {
  accent-color: rebeccapurple;
}

Notez que la couleur d'arrière-plan de la piste passe du clair au foncé en fonction de accent-color. Le navigateur garantit un contraste approprié, ce qui est plutôt pratique.

Couleurs claires et sombres entièrement personnalisées

Définissez deux propriétés personnalisées sur l'élément <progress>, l'une pour la couleur de la piste et l'autre pour la couleur de progression de la piste. Dans la requête média prefers-color-scheme, indiquez de nouvelles valeurs de couleur pour la piste et la progression de la piste.

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

Styles de sélection

Plus tôt, nous avons attribué un index de tabulation négatif à l'élément pour qu'il puisse être sélectionné de manière programmatique. Utilisez :focus-visible pour personnaliser la sélection et activer le style d'anneau de sélection plus intelligent. Ainsi, un clic de souris et le focus n'afficheront pas l'anneau de focus, mais les clics au clavier l'afficheront. La vidéo YouTube aborde ce sujet plus en détail et mérite d'être visionnée.

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

Capture d'écran de la barre de chargement entourée d'un anneau de sélection. Les couleurs sont toutes assorties.

Styles personnalisés sur tous les navigateurs

Personnalisez les styles en sélectionnant les parties d'un élément <progress> exposées par chaque navigateur. L'élément progress est une balise unique, mais il est composé de quelques éléments enfants exposés via des pseudo-sélecteurs CSS. Les Outils pour les développeurs Chrome vous montreront ces éléments si vous activez le paramètre :

  1. Effectuez un clic droit sur votre page, puis sélectionnez Inspecter pour afficher les outils de développement.
  2. Cliquez sur l'icône des paramètres en forme de roue dentée en haut à droite de la fenêtre des outils de développement.
  3. Sous l'en-tête Elements (Éléments), recherchez et cochez la case Show user agent shadow DOM (Afficher le DOM fantôme de l'user-agent).

Capture d'écran de l'emplacement dans les outils de développement où activer l'exposition du DOM fantôme de l'user-agent.

Styles Safari et Chromium

Les navigateurs basés sur WebKit, tels que Safari et Chromium, exposent ::-webkit-progress-bar et ::-webkit-progress-value, qui permettent d'utiliser un sous-ensemble de CSS. Pour l'instant, définissez background-color à l'aide des propriétés personnalisées créées précédemment, qui s'adaptent aux thèmes clair et sombre.

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

Capture d'écran montrant les éléments internes de l'élément de progression.

Styles Firefox

Firefox n'expose le pseudo-sélecteur ::-moz-progress-bar que sur l'élément <progress>. Cela signifie également que nous ne pouvons pas teinter la piste directement.

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

Capture d'écran de Firefox et de l'emplacement des éléments de progression.

Capture d'écran du Debugging Corner où Safari, Safari pour iOS, Firefox, Chrome et Chrome pour Android affichent tous la barre de chargement en cours de fonctionnement.

Notez que Firefox a une couleur de piste définie à partir de accent-color, tandis que Safari sur iOS a une piste bleu clair. Il en va de même en mode sombre : Firefox a une piste sombre, mais pas la couleur personnalisée que nous avons définie, et cela fonctionne dans les navigateurs basés sur Webkit.

Animation

Lorsque vous utilisez des pseudo-sélecteurs intégrés au navigateur, vous disposez souvent d'un ensemble limité de propriétés CSS autorisées.

Animation de la jauge de progression

L'ajout d'une transition à l'élément inline-size de l'élément de progression fonctionne pour Chromium, mais pas pour Safari. Firefox n'utilise pas non plus de propriété de transition sur son ::-moz-progress-bar.

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

Animer l'état :indeterminate

Ici, je fais preuve d'un peu plus de créativité pour pouvoir fournir une animation. Un pseudo-élément pour Chromium est créé, et un dégradé est appliqué. Il est animé d'avant en arrière pour les trois navigateurs.

Les propriétés personnalisées

Les propriétés personnalisées sont très utiles pour de nombreuses choses, mais l'une de mes préférées est simplement de donner un nom à une valeur CSS qui semble magique. Voici un linear-gradient assez complexe, mais avec un nom agréable. Son objectif et ses cas d'utilisation sont clairement compréhensibles.

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

Les propriétés personnalisées permettent également de respecter le principe DRY, car nous ne pouvons pas regrouper ces sélecteurs spécifiques au navigateur.

Les images clés

L'objectif est d'obtenir une animation infinie qui va et vient. Les images clés de début et de fin seront définies en CSS. Une seule image clé est nécessaire, l'image clé du milieu à 50%, pour créer une animation qui revient à son point de départ, encore et encore.

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

Cibler chaque navigateur

Tous les navigateurs ne permettent pas de créer des pseudo-éléments sur l'élément <progress> lui-même ni d'animer la barre de progression. Plus de navigateurs sont compatibles avec l'animation de la piste qu'avec un pseudo-élément. Je passe donc des pseudo-éléments comme base à l'animation des barres.

Pseudo-élément Chromium

Chromium autorise le pseudo-élément ::after utilisé avec une position pour couvrir l'élément. Les propriétés personnalisées indéterminées sont utilisées, et l'animation de va-et-vient fonctionne très bien.

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);
}
Barre de progression Safari

Pour Safari, les propriétés personnalisées et une animation sont appliquées à la barre de progression du pseudo-élément :

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Barre de progression Firefox

Pour Firefox, les propriétés personnalisées et une animation sont également appliquées à la barre de progression du pseudo-élément :

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 joue un rôle important avec l'élément <progress>. Il contrôle la valeur envoyée à l'élément et s'assure que le document contient suffisamment d'informations pour les lecteurs d'écran.

const state = {
  val: null
}

La démo propose des boutons pour contrôler la progression. Ils mettent à jour state.val, puis appellent une fonction pour mettre à jour le DOM.

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

setProgress()

C'est dans cette fonction que l'orchestration de l'UI/UX se produit. Commencez par créer une fonction setProgress(). Aucun paramètre n'est nécessaire, car il a accès à l'objet state, à l'élément de progression et à la zone <main>.

const setProgress = () => {
  
}

Définition de l'état de chargement sur la zone <main>…

Selon que la progression est terminée ou non, l'élément <main> associé doit être mis à jour avec l'attribut aria-busy :

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

Effacer les attributs si le montant du chargement est inconnu

Si la valeur est inconnue ou non définie, null dans ce cas, supprimez les attributs value et aria-valuenow. La valeur de <progress> sera alors indéterminée.

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

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

Résoudre les problèmes de calcul décimal en JavaScript

Comme j'ai choisi de conserver la valeur maximale par défaut de 1, les fonctions d'incrémentation et de décrémentation de la démo utilisent des calculs décimaux. JavaScript et d'autres langages ne sont pas toujours très performants dans ce domaine. Voici une fonction roundDecimals() qui supprime l'excédent du résultat mathématique :

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

Arrondissez la valeur pour qu'elle soit lisible et puisse être présentée :

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

Définir la valeur pour les lecteurs d'écran et l'état du navigateur

La valeur est utilisée à trois emplacements dans le DOM :

  1. Attribut value de l'élément <progress>.
  2. Attribut aria-valuenow.
  3. Contenu textuel interne de <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
}

Mettre l'accent sur la progression

Une fois les valeurs mises à jour, les utilisateurs voyants verront la progression changer, mais les utilisateurs de lecteurs d'écran ne seront pas encore informés du changement. Sélectionnez l'élément <progress>, et le navigateur annoncera la mise à jour.

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()
}

Capture d'écran de l'application VoiceOver de macOS lisant la progression de la barre de chargement à l'utilisateur.

Conclusion

Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 🙂

Il y a certainement quelques modifications que j'aimerais apporter si j'en avais l'occasion. Je pense qu'il est possible de nettoyer le composant actuel et d'essayer d'en créer un sans les limites de style de pseudo-classe de l'élément <progress>. Cela vaut la peine d'essayer !

Diversifions nos approches et découvrons toutes les façons de créer sur le Web.

Créez une démo, envoyez-moi les liens par tweet et je l'ajouterai à la section des remix de la communauté ci-dessous.

Remix par la communauté