Créer un composant de barre de chargement

Présentation de base sur la création d'une barre de chargement adaptative et accessible à l'aide de l'élément <progress>.

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

Démonstration des états clair et sombre, indéterminé, croissant et terminé dans Chrome.

Si vous préférez regarder une 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é pour moderniser le composant et l'adapter mieux aux systèmes de conception.

Onglets clairs et sombres dans chaque navigateur offrant une vue d&#39;ensemble de l&#39;icône adaptative de haut en bas : Safari, Firefox et Chrome.
Démonstration diffusée dans Firefox, Safari, iOS Safari, Chrome et Chrome pour Android, avec des schémas 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 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. Si vous définissez max sur 100, par exemple, la plage sera comprise entre 0 et 100. J'ai choisi de rester dans les limites de 0 et 1, en traduisant les valeurs de progression en 0,5 ou 50%.

Progression encapsulée par un libellé

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

<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 é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 montrant l&#39;élément &quot;Ready only&quot; (Prêt uniquement).

Zone affectée par la progression du chargement

Si vous avez une bonne vision, 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 aussi clair pour les utilisateurs ayant une déficience visuelle. 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.

Ajout 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 ne disposent pas de 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 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 en plaçant le focus sur la progression lorsque la progression change, cela annonce à l'utilisateur le degré d'avancement de la progression mise à jour.

Styles

L'élément de progression est un peu délicat à styliser. 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 et la position de l'élément de progression. Un état de finalisation spécial est ajouté, qui peut être un indice 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'il puisse se réduire et se développer 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 dans les différents navigateurs, car chacun d'eux a ses propres styles pour ses éléments.

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 1e3px pour _radius utilise la notation 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 élevée pour pouvoir la définir et l'oublier (et c'est plus court à écrire que 1000px). Il est également facile de l'augmenter encore si nécessaire : il suffit de remplacer le chiffre 3 par un chiffre 4, puis 1e4px est équivalent à 10000px.

overflow: hidden est utilisé et a toujours été un style controversé. Cela a simplifié certaines choses, par exemple le fait de ne pas avoir à transmettre des valeurs border-radius à la piste et aux éléments de remplissage de la piste. En revanche, cela signifiait 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é peut être effectuée sans overflow: hidden, ce qui peut permettre d'obtenir des animations ou de meilleurs états de fin.

Processus terminé

Les sélecteurs CSS effectuent la tâche difficile en comparant la valeur maximale à la valeur. 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 bon indice visuel supplémentaire pour la finalisation.

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 % avec 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. Il peut être développé à l'aide de sélecteurs spécifiques à chaque 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 de remplissage de la progression dans une propriété unique

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

progress {
  accent-color: rebeccapurple;
}

Notez que la couleur d'arrière-plan du canal passe du clair au sombre en fonction de l'accent-color. Le navigateur assure un contraste approprié: c'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 du canal et l'autre pour la couleur de la progression du canal. Dans la requête multimédia prefers-color-scheme, fournissez de nouvelles valeurs de couleur pour le titre et la progression du titre.

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 mise au point

Auparavant, nous avons attribué à l'élément un indice de tabulation négatif afin qu'il puisse être mis au premier plan par programmation. Utilisez :focus-visible pour personnaliser la sélection et activer le style d'anneau de sélection plus intelligent. Dans ce cas, un clic de souris et la sélection ne déclenchent pas l'anneau de sélection, mais les clics au clavier le font. La vidéo YouTube explique cela plus en détail et vaut la peine d'être regardée.

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

Capture d&#39;écran de la barre de chargement avec un anneau de mise au point autour. Les couleurs correspondent toutes.

Styles personnalisés dans les 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 ne nécessite qu'une seule balise, mais elle est composée de quelques éléments enfants exposés via des pseudo-sélecteurs CSS. Les outils de développement Chrome vous montreront ces éléments si vous activez le paramètre:

  1. Effectuez un clic droit sur votre page, puis sélectionnez Inspecter l'élément pour ouvrir les outils pour les développeurs.
  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 de l&#39;emplacement dans les outils pour les développeurs où activer l&#39;exposition du DOM fantôme de l&#39;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&#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 sur 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 emplacement des parties de l&#39;élément de progression.

Capture d&#39;écran du coin de débogage où la barre de chargement de Safari, Safari sur iOS, Firefox, Chrome et Chrome sur Android fonctionne.

Notez que Firefox utilise la couleur accent-color pour la piste, tandis qu'iOS Safari utilise une piste 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

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

Animer le remplissage du canal

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 suis un peu plus créatif et je peux proposer une animation. Un pseudo-élément pour Chromium est créé et un dégradé est appliqué, qui est animé en avant et en arrière 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 nom agréable. Son objectif et ses cas d'utilisation sont 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 de garder le code DRY, car nous ne pouvons pas regrouper ces sélecteurs spécifiques 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, 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 de 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 de base aux barres d'animation.

Pseudo-élément Chromium

Chromium autorise le pseudo-élément ::after utilisé avec une position pour recouvrir 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 de 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 qu'il y a suffisamment d'informations dans le document pour les lecteurs d'écran.

const state = {
  val: null
}

La démonstration 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 s'effectue l'orchestration de l'UI/de l'expérience utilisateur. 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éfinir l'état de chargement dans la zone <main>

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

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

Supprimer les attributs si la quantité de chargement est inconnue

Si la valeur est inconnue ou non définie, null dans cet usage, supprimez les attributs value et aria-valuenow. L'état <progress> devient alors 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 calcul décimal 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 pour qu'elle puisse être 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

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

  1. Attribut value de l'élément <progress>.
  2. Attribut aria-valuenow.
  3. Contenu du texte interne <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 en avant la progression

Une fois les valeurs mises à jour, les utilisateurs voyants verront le changement de progression, mais les utilisateurs de lecteurs d'écran ne recevront pas encore l'annonce du changement. Mettez en surbrillance 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&#39;écran de l&#39;application VoiceOver de Mac OS lisant 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'essayer d'en créer un sans les limites de style de la pseudo-classe de l'élément <progress>. Cela vaut la peine d'explorer cette option.

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

Créez une démo, tweetez-moi des liens et je les ajouterai à la section "Remix de la communauté" ci-dessous.

Remix de la communauté