Créer un composant de barre de chargement

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

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

Démonstration de clair et sombre, indéterminée, croissante et complète sur Chrome.

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

Présentation

L'élément <progress> fournit aux utilisateurs des commentaires visuels et audibles à propos de l'achèvement. Ce retour visuel est utile pour des scénarios tels que la progression dans un formulaire, l'affichage d'informations sur le téléchargement ou l'importation, ou même pour montrer que la quantité de progression est inconnue, mais que le travail est toujours en cours.

Pour ce défi IUG, nous avons utilisé 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 affichant un aperçu de l&#39;icône adaptative de haut en bas : Safari, Firefox et Chrome.
Démonstration dans Firefox, Safari, iOS, Safari, Chrome et Chrome pour Android, avec des schémas clairs et sombres.

Markup

J'ai choisi d'encapsuler l'élément <progress> dans un <label> afin d'ignorer les attributs de relation explicite 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>

En l'absence 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 vous définissez max sur 100, la plage va de 0 à 100. J'ai choisi de rester entre les limites 0 et 1, en traduisant les valeurs de progression en 0,5 ou 50%.

Progression de l'encapsulation du libellé

Dans une relation implicite, un élément de progression est encapsulé par une étiquette comme celle-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 appliquez-lui des styles pour qu'il se trouve hors de l'écran:

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

Avec le CSS associé suivant, tiré 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&#39;écran des outils de développement révélant l&#39;élément &quot;Compatible uniquement avec l&#39;écran&quot;.

Zone concernée par la progression du chargement

Si vous avez une vision saine, il peut être facile d'associer un indicateur de progression à des éléments et des zones de page associés, mais pour les utilisateurs déficients visuels, ce n'est pas si clair. Pour améliorer ce résultat, attribuez l'attribut aria-busy à l'élément supérieur qui sera modifié une fois le chargement terminé. Vous pouvez également indiquer 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, 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 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 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 le fait de se concentrer sur la progression à mesure que la progression évolue permet d'indiquer à l'utilisateur le degré de progression de la mise à jour.

Styles

L'élément de progression est un peu compliqué en termes de 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 des libellés. 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 de <progress>

La largeur de l'élément de progression reste inchangée afin qu'elle puisse se réduire et s'agrandir avec l'espace nécessaire dans la conception. Les styles intégrés sont supprimés en définissant appearance et border sur none. L'élément peut ainsi être normalisé dans tous 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, afin que la valeur border-radius soit toujours arrondie. Cela équivaut à 1000px. J'aime utiliser cette option, car mon objectif est d'utiliser une valeur suffisamment grande pour pouvoir la définir et l'oublier (et elle est plus courte à écrire que 1000px). Il est également facile de l'agrandir encore davantage si nécessaire: remplacez simplement le 3 par 4, puis 1e4px équivaut à 10000px.

overflow: hidden est utilisé et constitue un style contentieux. Cela facilitait certains aspects, comme ne pas avoir besoin de transmettre de valeurs border-radius au titre ni de suivre les éléments de remplissage, mais aussi 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 animations ou de meilleurs états d'achèvement.

Processus terminé

Les sélecteurs CSS font le travail difficile ici en comparant le maximum à la valeur. S'ils 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 bel indice visuel supplémentaire vers l'achèvement.

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 ajoute ses propres couleurs pour l'élément de progression, et s'adapte au clair et à l'obscurité avec une seule propriété CSS. Il peut être basé sur des sélecteurs spécifiques à chaque navigateur.

Styles de navigateur clairs et sombres

Pour activer un élément <progress> adaptatif sombre et clair sur votre site, il vous suffit de 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 sombre en fonction de accent-color. Le navigateur assure un contraste correct: il est assez soigné.

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 suivi et l'autre pour la couleur de sa progression. Dans la requête média prefers-color-scheme, indiquez de nouvelles valeurs de couleur pour le suivi et 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 mise au point

Précédemment, nous avons attribué à l'élément un index de tabulation négatif pour qu'il puisse être sélectionné par programmation. Utilisez :focus-visible pour personnaliser la mise au point et adopter le style de l'anneau de mise au point plus intelligent. Ainsi, un clic de souris et un focus n'afficheront pas l'anneau de sélection, mais les clics sur le clavier l'afficheront. La vidéo YouTube approfondit le sujet et mérite d'être examinée.

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 sélection. Toutes les couleurs s&#39;accordent.

Styles personnalisés pour tous les navigateurs

Personnalisez les styles en sélectionnant les parties d'un élément <progress> que chaque navigateur expose. L'élément de progression nécessite une seule balise, mais il est composé de quelques éléments enfants exposés via des sélecteurs CSS. Les outils pour les développeurs Chrome affichent ces éléments si vous activez ce 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 Paramètres (en forme de roue dentée) dans l'angle supérieur droit de la fenêtre "Outils de développement".
  3. Sous le titre Éléments, recherchez et cochez la case Afficher le user-agent Shadow DOM.

Capture d&#39;écran de l&#39;emplacement dans les outils de développement où activer l&#39;exposition du user-agent Shadow DOM.

Styles Safari et Chromium

Les navigateurs basés sur WebKit tels que Safari et Chromium exposent ::-webkit-progress-bar et ::-webkit-progress-value, ce qui permet 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 modes 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 teinter la piste directement.

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

Capture d&#39;écran de Firefox et de l&#39;emplacement des parties de l&#39;élément de progression.

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

Notez que dans Firefox, une couleur de tracé est définie sur accent-color, tandis que celle d'iOS Safari a une couleur bleu clair. C'est la même chose en mode sombre: Firefox a une piste sombre, mais pas la couleur personnalisée que nous avons définie, et il fonctionne dans les navigateurs WebKit.

Animation

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

Animation montrant que la piste se remplit

L'ajout d'une transition au 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 fais preuve d'un peu plus de créativité pour pouvoir fournir une animation. Un pseudo-élément est créé pour Chromium, puis un dégradé est appliqué. Il est animé à la suite dans les trois navigateurs.

Les propriétés personnalisées

Les propriétés personnalisées sont utiles pour beaucoup de choses, mais l'une de mes préférées est simplement de donner un nom à une valeur CSS à l'aspect magique. Vous trouverez ci-dessous un linear-gradient assez complexe, mais avec un joli nom. Son objectif et ses cas d'utilisation peuvent ê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 aident également le code à rester DRY, car nous ne pouvons pas encore regrouper ces sélecteurs spécifiques au navigateur.

Les images clés

L'objectif est 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, 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 sur l'élément <progress> lui-même ou n'autorisent pas 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 en tant que base à des barres d'animation.

Pseudo-élément Chromium

Chromium autorise le pseudo-élément ::after utilisé avec une position permettant de recouvrir l'élément. Les propriétés personnalisées indéterminées sont utilisées, et l'animation 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>. Elle contrôle la valeur envoyée à l'élément et garantit que suffisamment d'informations sont présentes dans le document 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'interface utilisateur et 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 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 non définie, supprimez les attributs value et aria-valuenow pour cette valeur null. La <progress> devient 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 maths décimaux JavaScript

Comme j'ai choisi de conserver le maximum de progression par défaut de 1, les fonctions d'incrémentation et de décrémentation de démonstration utilisent des calculs décimaux. JavaScript et d'autres langages ne sont pas toujours efficaces. Voici une fonction roundDecimals() qui supprimera le résultat mathématique en trop:

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 une valeur pour les lecteurs d'écran et l'état du navigateur

Cette valeur est utilisée à trois emplacements 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 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 recevront pas encore l'annonce du 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 Voice Over pour Mac OS qui lit la progression de la barre de chargement à l&#39;utilisateur.

Conclusion

Maintenant que vous savez comment je l'ai fait, comment le feriez-vous‽ 😃 ?

Si j'ai encore une chance, j'aimerais apporter quelques modifications. 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 de l'explorer !

Diversissons nos approches et apprenons toutes les façons de créer sur le Web.

Créez une démo, cliquez sur les liens tweet me, et je l'ajouterai à la section "Remix" de la communauté ci-dessous.

Remix de la communauté