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 ma réflexion sur la façon de créer une barre de chargement accessible et adaptative aux couleurs avec l'élément <progress>
. Essayez la démo et consultez la source.
Si vous préférez les vidéos, voici une version YouTube de cet article :
Présentation
L'élément
<progress>
fournit aux utilisateurs un retour visuel et audio sur l'état d'avancement. Ce retour visuel est utile dans les scénarios suivants : progression dans un formulaire, affichage des informations de téléchargement ou d'importation, ou même indication que le montant de la progression est inconnu, mais que le travail est toujours en cours.
Ce défi d'interface utilisateur graphique a fonctionné avec l'élément HTML <progress>
existant pour économiser des efforts en termes d'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.

Annoter
J'ai choisi d'encapsuler l'élément <progress>
dans un <label>
pour pouvoir ignorer les attributs de relation explicites au profit d'une relation implicite.
J'ai également libellé un élément parent affecté par l'état de chargement, afin que les technologies de lecture d'écran puissent relayer 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 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 en 0,5 ou 50 %.
Progression avec 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émo, j'ai choisi d'inclure le libellé pour les lecteurs d'écran uniquement.
Pour ce faire, le texte du libellé est encapsulé dans un <span>
et des styles lui sont appliqués pour qu'il soit effectivement hors écran :
<label>
<span class="sr-only">Loading progress</span>
<progress></progress>
</label>
Avec le code CSS WebAIM suivant :
.sr-only {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Zone concernée par la progression du chargement
Si votre vision est normale, il peut être facile d'associer un indicateur de progression aux éléments et aux zones de la page qui lui sont associés. Mais pour les utilisateurs malvoyants, ce n'est pas aussi évident. Pour améliorer cela, 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>
À partir de JavaScript, définissez 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
, 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 définir explicitement l'état de l'élément sur "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 donnant la priorité à la progression à mesure qu'elle change, l'utilisateur est informé de l'état d'avancement.
Styles
L'élément de progression est un peu délicat en termes de style. Les éléments HTML intégrés comportent des parties masquées spéciales qui peuvent être difficiles à sélectionner et qui n'offrent souvent qu'un ensemble limité de propriétés à définir.
Disposition
Les styles de mise en page sont conçus pour offrir une certaine flexibilité en termes de taille de l'élément de progression et de position du libellé. Un état d'achèvement spécial est ajouté. Il peut s'agir d'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'il puisse 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 dans 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 des nombres pour exprimer un grand nombre. Par conséquent, border-radius
est toujours arrondi. Cela équivaut à 1000px
. J'aime utiliser cette valeur, 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'augmenter encore si nécessaire : il suffit de remplacer 3 par 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
aux éléments de remplissage de piste et de piste. Mais cela signifiait également qu'aucun enfant de la progression ne pouvait vivre 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 gros du travail ici en comparant le maximum à la valeur. Si les deux correspondent, la progression est terminée. Une fois la tâche 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.
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);
}
Couleur
Le navigateur apporte ses propres couleurs à l'élément de progression et s'adapte aux modes clair et sombre avec une seule propriété CSS. Vous pouvez vous appuyer sur des sélecteurs spéciaux spécifiques à certains navigateurs.
Styles de navigateur clair et sombre
Pour activer un élément <progress>
adaptatif en mode sombre et clair sur votre site, il vous suffit d'ajouter 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 de claire à foncée 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 la 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
Nous avons précédemment attribué à l'élément un index de tabulation négatif afin qu'il puisse être sélectionné 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, contrairement aux clics au clavier. La vidéo YouTube aborde ce point plus en détail et mérite d'être visionnée.
progress:focus-visible {
outline-color: var(--_progress);
outline-offset: 5px;
}
Styles personnalisés dans les navigateurs
Personnalisez les styles en sélectionnant les parties d'un élément <progress>
que chaque navigateur expose. L'élément de progression est une balise unique, mais il est composé de plusieurs éléments enfants qui sont exposés via des pseudo-sélecteurs CSS. Les outils de développement Chrome vous afficheront ces éléments si vous activez le paramètre :
- Effectuez un clic droit sur votre page, puis sélectionnez Inspecter pour afficher les outils de développement.
- Cliquez sur l'icône en forme de roue dentée en haut à droite de la fenêtre des outils de développement.
- Sous l'en-tête Éléments, recherchez et cochez la case Afficher le DOM fantôme de l'agent utilisateur.
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 modes clair et sombre.
/* Safari/Chromium */
progress[value]::-webkit-progress-bar {
background-color: var(--_track);
}
progress[value]::-webkit-progress-value {
background-color: var(--_progress);
}
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 directement la piste.
/* Firefox */
progress[value]::-moz-progress-bar {
background-color: var(--_progress);
}
Notez que Firefox a défini une couleur de piste à partir de accent-color
, tandis qu'iOS Safari a une piste 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 cela fonctionne dans les navigateurs basés sur Webkit.
Animation
Lorsque vous travaillez avec des pseudo-sélecteurs intégrés au navigateur, il s'agit souvent d'un ensemble limité de propriétés CSS autorisées.
Animer le remplissage de la piste
L'ajout d'une transition à 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 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 vais faire 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é, qui est animé d'avant en arrière pour les trois navigateurs.
Propriétés personnalisées
Les propriétés personnalisées sont utiles pour de nombreuses choses, mais l'une de mes préférées consiste 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 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 aideront également le code à rester DRY, car, encore une fois, nous ne pouvons pas regrouper ces sélecteurs spécifiques au navigateur.
Images clés
L'objectif est de créer 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 prennent en charge l'animation de la piste qu'un pseudo-élément. Je passe donc des pseudo-éléments comme base aux 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 d'avant et arrière 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 permettant de 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 a lieu. 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 être mis à jour pour 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 cet usage, supprimez les attributs value
et aria-valuenow
. L'état de <progress>
passera à "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 la valeur maximale par défaut de 1 pour la progression, 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 efficaces.
Voici une fonction roundDecimals()
qui supprimera 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 puisse être présentée et soit 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 endroits dans le DOM :
- Attribut
value
de l'élément<progress>
. - Attribut
aria-valuenow
. - 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
}
Donner la priorité à 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. Faites en sorte que l'élément <progress>
soit sélectionné. 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()
}
Conclusion
Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 🙂
Si j'en avais l'occasion, j'aimerais apporter quelques modifications. Je pense qu'il est possible de nettoyer le composant actuel et d'en créer un sans les limitations de style de pseudo-classe de l'élément <progress>
. N'hésitez pas à l'explorer !
Diversifions nos approches et découvrons toutes les façons de créer sur le Web.
Créez une démo, tweetez-moi les liens et je l'ajouterai à la section des remix de la communauté ci-dessous !