Créer un composant de barre de chargement

Présentation générale de la création d'une barre de chargement accessible et adaptable aux couleurs avec l'élément <progress>.

Dans cet article, je vais vous expliquer comment créer une barre de chargement aux couleurs adaptative et accessible avec l'élément <progress>. Essayez la démonstration et consultez la source.

Démonstration : clair et sombre, indéterminé, croissant et terminé sur Chrome.

Si vous préférez la vidéo, voici une version YouTube de cet article:

Présentation

L'élément <progress> fournit aux utilisateurs des commentaires visuels et sonores concernant l'achèvement. Ces commentaires visuels sont utiles pour les scénarios tels que la progression dans un formulaire, l'affichage d'informations de téléchargement ou d'importation, ou même si la durée de la progression est inconnue, mais que le travail est toujours actif.

Ce défi de l'IUG a fonctionné avec l'élément HTML <progress> existant pour simplifier 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 l'adapter aux systèmes de conception.

Onglets clair et sombre de chaque navigateur, offrant un aperçu de l&#39;icône adaptative de haut en bas : Safari, Firefox et Chrome.
Démonstration affichée dans Firefox, Safari, iOS Safari, Chrome et Chrome pour Android, avec des schémas clair et sombre.

Markup

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 ajouté un libellé à un élément parent affecté par l'état de chargement, afin que les technologies de lecteur d'écran puissent transmettre ces informations à un utilisateur.

<progress></progress>

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

Progression de l'encapsulation par étiquette

Dans une relation implicite, un élément de progression est encapsulé par un libellé semblable à 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 une <span> et lui appliquez des styles afin qu'il se trouve hors écran:

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

Avec le code CSS suivant de WebAIM associé:

.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&#39;écran des outils de développement révélant l&#39;élément &quot;Screen ready only&quot; (prêt à l&#39;écran uniquement).

Zone affectée par la progression du chargement

Si votre vision est de bonne qualité, il peut être facile d'associer un indicateur de progression aux éléments et aux zones de page associés, mais ce n'est pas si clair pour les utilisateurs malvoyants. Pour améliorer cela, attribuez l'attribut aria-busy au premier élément qui sera modifié une fois le chargement terminé. De plus, indiquez 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>

À partir de JavaScript, basculez aria-busy sur true au début de la tâche et 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, nous l'avons rendu explicite pour les navigateurs qui ne disposent pas de ce rôle. 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 de 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. C'est important pour la technologie de lecteur d'écran, car l'activation de l'indicateur de progression lorsque la progression change, annonce à l'utilisateur le degré d'avancement de la progression mise à jour.

Styles

L'élément de progression est un peu complexe au niveau du style. Les éléments HTML intégrés comportent des parties cachées spéciales qui peuvent être difficiles à sélectionner et n'offrent souvent qu'un ensemble limité de propriétés à définir.

Mise en page

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

Mise en page <progress>

La largeur de l'élément de progression n'est pas modifiée afin qu'elle puisse être réduite et agrandie avec l'espace nécessaire dans la conception. Les styles intégrés sont supprimés en définissant appearance et border sur none. Ainsi, l'élément peut être normalisé entre les navigateurs, car chaque navigateur possède son propre style 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 de nombre scientifique pour exprimer un grand nombre. border-radius est donc toujours arrondi. Cela équivaut à 1000px. J'aime utiliser cette méthode, car mon objectif est d'utiliser une valeur suffisamment grande pour pouvoir la définir et l'oublier (et elle est plus courte que 1000px). Il est également facile de la rendre encore plus grande si nécessaire: remplacez simplement 3 par 4, puis 1e4px équivaut à 10000px.

overflow: hidden est utilisé et constitue un style litigieux. Cela a facilité certains choses, comme le fait de ne pas avoir à transmettre les valeurs border-radius à la piste et à suivre les éléments de remplissage, mais 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 permettre d'obtenir des animations ou de meilleurs états de fin.

Processus terminé

Dans ce cas, les sélecteurs CSS sont les plus complexes en comparant le maximum à la valeur. Si ces valeurs 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 constitue un indicateur visuel supplémentaire de 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&#39;écran de la barre de chargement à 100% et montrant une coche à la fin

Couleur

Le navigateur apporte ses propres couleurs pour l'élément de progression et s'adapte aux couleurs claires et sombres avec une seule propriété CSS. Cette approche peut être développée à l'aide de sélecteurs spécifiques au navigateur.

Styles de navigateur clair et sombre

Pour ajouter à votre site un élément <progress> adaptatif sombre et clair, il vous suffit de color-scheme.

progress {
  color-scheme: light dark;
}

Couleur remplie de la progression d'une propriété unique

Pour colorer 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 assure le contraste, ce qui est plutôt net.

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 la progression. Dans la requête média prefers-color-scheme, fournissez de nouvelles valeurs de couleur pour le parcours et suivez la progression.

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 focus

Nous avons précédemment attribué à l'élément un index d'onglet négatif afin qu'il puisse être sélectionné de manière programmatique. Utilisez :focus-visible pour personnaliser la mise au point et activer le style de l'anneau de mise au point plus intelligent. Ainsi, un clic de souris et un focus n'affichent pas l'anneau de focus, contrairement aux clics du clavier. Pour en savoir plus, regardez la vidéo YouTube qui s'y rapporte.

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

Capture d&#39;écran de la barre de chargement entourée d&#39;un anneau de focus. Toutes les couleurs correspondent.

Styles personnalisés dans les différents navigateurs

Personnalisez les styles en sélectionnant les parties d'un élément <progress> que chaque navigateur expose. L'utilisation de l'élément de progression correspond à une seule balise, mais elle est constituée de quelques éléments enfants exposés via des pseudo-sélecteurs CSS. Les outils pour les développeurs Chrome vous présentent les éléments suivants si vous activez le paramètre:

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

Capture d&#39;écran montrant l&#39;emplacement dans les outils de développement pour permettre l&#39;exposition du Shadow DOM du user-agent.

Styles Safari et Chromium

Les navigateurs basés sur WebKit tels que Safari et Chromium proposent ::-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 au clair et au mode sombre.

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

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

Capture d&#39;écran montrant les éléments internes de l&#39;élément de progression.

Styles Firefox

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

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

Capture d&#39;écran de Firefox et où se trouvent les éléments de progression.

Capture d&#39;écran de l&#39;angle de débogage où la barre de chargement est affichée dans Safari, iOS Safari, Firefox, Chrome et Chrome sur Android.

Notez que la couleur de suivi de Firefox est définie à partir de accent-color, tandis que celle de Safari pour iOS est en bleu clair. Il en va de même en mode sombre: Firefox dispose d'une piste sombre, mais n'utilise pas la couleur personnalisée que nous avons définie. De plus, cela fonctionne dans les navigateurs WebKit.

Animation

Bien qu'il fonctionne avec des pseudo-sélecteurs intégrés au navigateur, il est souvent associé à un ensemble limité de propriétés CSS autorisées.

Animer la piste qui se remplit

L'ajout d'une transition vers la 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 ::-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 suis un peu plus créatif et je peux proposer une animation. Un pseudo-élément est créé pour Chromium, puis appliqué un dégradé qui est animé dans les deux sens pour les trois navigateurs.

Propriétés personnalisées

Les propriétés personnalisées sont utiles pour de nombreux aspects, mais l'une de mes préférées est de simplement donner un nom à une valeur CSS qui semble magique. Voici un linear-gradient assez complexe, mais avec un joli nom. Son objectif et ses cas d'utilisation doivent être clairement compris.

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 au code de rester DRY, car il n'est pas possible de regrouper ces sélecteurs propres au navigateur.

Les images clés

L'objectif est une animation infinie qui va dans les deux sens. Les images clés de début et de fin seront définies en CSS. Une seule image clé est nécessaire, celle 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 n'autorisent pas la création de pseudo-éléments au niveau de l'élément <progress> lui-même, ni l'animation de la barre de progression. Davantage de navigateurs prennent en charge l'animation de la piste qu'un pseudo-élément. Je passe donc des pseudo-éléments comme base à des barres d'animation.

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 arrière et suivante 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

Dans 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>. Elle contrôle la valeur envoyée à l'élément et garantit que le document contient suffisamment d'informations pour les lecteurs d'écran.

const state = {
  val: null
}

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

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

setProgress()

C'est dans cette fonction que s'effectue l'orchestration de l'UI/de l'expérience utilisateur. Pour commencer, créez 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éfinir 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 de l'attribut aria-busy:

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

Effacer les attributs si la quantité de chargement est inconnue

Si la valeur est inconnue ou n'est pas définie, null dans cette utilisation, supprimez les attributs value et aria-valuenow. Cela transformera <progress> en indéterminé.

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 mathématiques décimales JavaScript

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

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

Arrondissez la valeur de façon à ce qu'elle soit présentée et lisible:

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

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

  1. L'attribut value de l'élément <progress>.
  2. L'attribut aria-valuenow
  3. Le 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
}

Cibler la progression

Une fois les valeurs mises à jour, les utilisateurs voyants verront la progression changer, mais les utilisateurs de lecteurs d'écran ne sont pas encore informés de ce changement. Sélectionnez l'élément <progress> pour que le navigateur annonce 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&#39;écran de l&#39;application VoiceOver pour Mac OS qui lit la progression de la barre de chargement à l&#39;utilisateur.

Conclusion

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

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

Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web.

Créez une démo, envoyez-moi des tweets via des liens et je l'ajouterai à la section "Remix de la communauté" ci-dessous.

Remix de la communauté