[CSS-JS] Progress-Bar circulaire

NoSmoking

Dans cette page nous allons voir comment réaliser une « progress bar » circulaire en CSS, et comment se faciliter la vie avec l'utilisation du javascript.

Création de la Progress Bar

Nous allons voir les différents éléments mis en oeuvre pour la création de la « progress-bar » et le rôle de chacun.

Le fond de la gauge

Pour commencer nous allons créer un élément conteneur qui servira de fond à notre « progress-bar » circulaire, ce qui donne le code HTML suivant :

<div class="progress-circle" data-value="38">
</div>

Nous mettons également un attribut data-value dans lequel nous renseignons la valeur de la progression.

Le CSS affecté à cet élément est le suivant :

.progress-circle{
  position: relative;             /* pour servir de référent */
  box-sizing: border-box;         /* prise en compte bordure dans la dimension */
  font-size: 6em;                 /* pour définir les dimensions */
  width: 1em;                     /* fixe la largeur */
  height: 1em;                    /* fixe la hauteur */
  border-radius: 50%;             /* rendu aspect circulaire */
  border: .15em solid #CDE;       /* couleur de fond de l'anneau */
  background-color: #FFF;         /* couleur de fond de la progress bar */
}

La fenêtre d'affichage

En ajoutant, à l'élément précédent, une <div> nous mettons en place une fenêtre d'affichage, ici en pointillé, le code HTML devient :

<div class="progress-circle">
    <div class="progress-masque">
    </div>
</div>

Le CSS affecté à cet élément est le suivant :

.progress-masque {
  position: absolute;
  width: 1em;                     /* 100% de la largeur */
  height: 1em;                    /* 100% de la hauteur */
  left: -.15em;                   /* décalage de la largeur bordure de la gauge */
  top: -.15em;                    /* décalage de la largeur bordure de la gauge */
  clip: rect(0, 1em, 1em, .5em);  /* par défaut seule la partie droite est visible */
}

Lorsque la valeur de la progression est inférieure à 50%, on ne garde visible que ce qui se trouve dans la partie droite de la fenêtre, c'est le rôle de la déclaration clip: rect(0, 1em, 1em, .5em).

Lorsque la valeur de la progression est supérieure ou égale à 50%, on rétabli la visibilité sur la totalité de la fenêtre en appliquant la déclaration css suivante : clip: rect(auto, auto, auto, auto).

Nous allons utiliser l'attribut data-value de l'élément « progress-circle » pour gérer ce changement.

Dans la mesure où l'on souhaite cibler des valeurs ayant un pas de 0.1 cela se traduit par la création de la règle suivante :

.progress-circle[data-value^='5']:not([data-value='5']):not([data-value^='5.']) .progress-masque,
.progress-circle[data-value^='6']:not([data-value='6']):not([data-value^='6.']) .progress-masque,
.progress-circle[data-value^='7']:not([data-value='7']):not([data-value^='7.']) .progress-masque,
.progress-circle[data-value^='8']:not([data-value='8']):not([data-value^='8.']) .progress-masque,
.progress-circle[data-value^='9']:not([data-value='9']):not([data-value^='9.']) .progress-masque,
.progress-circle[data-value^='100'] .progress-masque {
  clip: rect(auto, auto, auto, auto);
}

Explication :

Chaque ligne est à interpréter de la façon suivante : tout élément possédant la class progress-masque, contenu dans un élément possédant la class progress-circle et qui possède un attribut data-value commençant par X mais qui n'a pas pour valeur X ou une valeur X avec une décimale, sera concerné par la déclaration.

Exemple :

Les valeurs 7, 7. et 7.2 ne seront pas concernées, alors que la valeur 71.3 le sera.

La barre de progression

En ajoutant une <div> à l'élément précédent, nous mettons en place l'élément « progress-barre », le code HTML devient :

<div class="progress-circle">
    <div class="progress-masque">
        <div class="progress-barre"></div>
    </div>
</div>

Le CSS affecté à cet élément est le suivant :

.progress-barre,
.progress-sup50 {
  position: absolute;
  box-sizing: border-box;         /* prise en compte bordure dans la dimension */
  border-width: .15em;            /* largeur bordure de la gauge */
  border-style: solid;
  border-color: #069;
  border-radius: 50%;             /* rendu aspect circulaire */
  width: 1em;                     /* largeur à 100% */
  height: 1em;                    /* hauteur à 100% */
  clip: rect(0, .5em, 1em, 0);    /* on ne garde que la partie gauche */
}

C'est cet élément qui permettra de visualiser la valeur de la progression.

On ne garde visible que la partie gauche en appliquant le css suivant : clip: rect(0, .5em, 1em, 0).

Suivant la valeur de la progression, le déplacement de l'élément se fera en modifiant sa propriété transform à l'aide de la valeur rotate(xxxdeg), où xxx est la valeur de l'angle en degrés.

La valeur de l'angle se calcul comme suit : angle = 360 * valeurProgression / 100.

Nous devons donc affecter une règle CSS pour chaque valeur de progression de 0 à 100.

Cela représente pas moins de 1203 lignes pour un poids d'environ 87Ko, « minifier », ce qui est somme toute assez lourd. C'est le prix à payer pour traiter toutes les valeurs à une décimale.

La barre des « 50% »

Pour finir nous mettons en place un élément utilisé pour l'affichage des valeurs supérieures ou égales à 50%. Ceci constitue le code HTML final de notre « progress bar ».

<div class="progress-circle">
    <div class="progress-masque">
        <div class="progress-barre"></div>
        <div class="progress-sup50"></div>
    </div>
</div>

Nous lui affectons la même règle CSS qu'à l'élément « progress-masque », les seules différences étant que nous ne gardons visible que la partie droite en lui appliquant la déclaration CSS clip: rect(0, 1em, 1em, .5em) et que cet élément est masqué par défaut.

Il est donc nécessaire de rajouter la règle CSS suivante :

.progress-sup50 {
  display: none;
  clip: rect(0, 1em, 1em, .5em);
}

Lorsque la valeur de progression est supérieure ou égale à 50%, l'élément est affiché en lui appliquant une déclaration display:block.

Comme pour l'élément « progress-masque », nous allons utiliser l'attribut data-value de l'élément « progress-circle » pour gérer ce changement, ce qui se traduit par la création de la règle suivante :

.progress-circle[data-value^='5']:not([data-value='5']):not([data-value^='5.']) .progress-sup50,
.progress-circle[data-value^='6']:not([data-value='6']):not([data-value^='6.']) .progress-sup50,
.progress-circle[data-value^='7']:not([data-value='7']):not([data-value^='7.']) .progress-sup50,
.progress-circle[data-value^='8']:not([data-value='8']):not([data-value^='8.']) .progress-sup50,
.progress-circle[data-value^='9']:not([data-value='9']):not([data-value^='9.']) .progress-sup50,
.progress-circle[data-value^='100'] .progress-sup50 {
  display:block;
}

Affichage de la valeur

Pour finir, nous allons afficher la valeur de la progression au centre de la « progress bar », pour cela nous allons utiliser le pseudo-élément CSS :after et récupérer la valeur qui est contenue dans l'attribut data-value de l'élément « progress-circle ».

La règle CSS apppliquée est la suivante :

.progress-circle:after {
  content: attr(data-value) "%";  /* récup. valeur de progression */
  font-size: 0.15em;              /* taille de la font en % du parent */
  height: 100%;                   /* centrage dans le parent */
  display: flex;
  align-items: center;
  justify-content: center;

  /*-- pour effet shadow intérieur --*/
  border-radius: 50%;
  box-shadow: 0 0 .5em rgba(0, 0, 0, .5) inset;
}

Le résultat en action

Déplacer le curseur pour voir le comportement des différents éléments.

On peut donc bien observer que pour les valeurs de progression inférieures à 50% :

Modification/personnalisation

Vous aurez peut-être besoin de modifier l'apparence de certains éléments de cette « progress bar », pour cela nous allons voir comment modifier différentes apparences en créant des classes.

Vous pouvez bien évidement modifier directement les règles par défaut.

Taille de l'élément

La modification de la taille de l'élément ce fait en modifiant la propriété font-size de la classe progress-circle.

Exemple : pour obtenir un élément ayant une hauteur/largeur de 80 pixels vous pouvez écrire la règle CSS suivante :

.progress-circle.small {
  font-size: 5em;                 /* soit 80px avec une font de 16px */
}
Il reste à ajouter dans le code HTML la classe small.
<div class="progress-circle small" data-value="38">

Couleur de fond de la barre

La modification de la couleur de fond de la barre de progression ce fait en modifiant la propriété border-color de la classe progress-circle.

Exemple : pour obtenir un fond de barre de progression rouge clair vous pouvez écrire la règle CSS suivante :

.progress-circle.red {
  border-color: #F8E0E0;          /* couleur de fond de la barre */
}
Il reste à ajouter dans le code HTML la classe red.
<div class="progress-circle red" data-value="38">

Couleur de la barre

La modification de la couleur de la barre de la progression ce fait en modifiant la propriété border-color de la classe progress-masque.

Exemple : pour obtenir une barre de progression rouge vous pouvez écrire la règle CSS suivante :

.progress-circle.red .progress-masque {
  border-color: #B40000;          /* couleur de la barre d'avancement */
}
Il reste à ajouter dans le code HTML la classe red.
<div class="progress-circle red" data-value="38">

Epaisseur de la barre

La modification de l'épaisseur de la barre de la progression est un peu plus complexe, car il nous faut modifier trois largeurs de bordure, pour les éléments « progress-circle », « progress-barre » et « progress-sup50 » et il est également nécesssaire de repositionner l'élément « progress-masque ».

Exemple : voici les règles à appliquer pour dimensionner la largeur de la bordure à 6px pour un élément de 80px de largeur/hauteur.

.progress-circle.thin,
.progress-circle.thin .progress-barre,
.progress-circle.thin .progress-sup50 {
  border-width: .075em;           /* largeur bordure de la gauge */
}
.progress-circle.thin .progress-masque {
  left: -.075em;                  /* largeur bordure gauge */
  top: -.075em;                   /* largeur bordure gauge */
}
Il reste à ajouter dans le code HTML la classe thin.
<div class="progress-circle thin" data-value="38">

Style affichage valeur

Pour modifier le style de l'affichage de la valeur de la progression on peut modifier les propriétés de la font de la classe progress-circle ainsi que la propriété color.

Exemple : pour obtenir un texte rouge et incliné vous pouvez écrire la règle CSS suivante :

.progress-circle.red {
  color: red;
  font-style: italic;
}

Attention : il ne faut pas modifier la propriété font-size qui détermine, elle, la hauteur/largeur de l'élément.

Quelques exemples

Création dynamique

Utilisation de javascript

Nous allons voir maintenant comment se faciliter la vie avec l'utilisation du javascript ce qui n'est pas une obligation.

Allégement du code HTML

On a vu que pour définir une « progress bar » il nous faut écrire le code HTML suivant :

<div class="progress-circle" data-value="38">
    <div class="progress-masque">
        <div class="progress-barre"></div>
        <div class="progress-sup50"></div>
    </div>
</div>

On admettra que cela n'est pas des plus intuitif, alors qu'écrire simplement

<div class="progress-circle" data-value="38"></div>

reste accessible à tous.

L'utilisation du javascript peut s'avèrer intéréssante pour la création des <div> incluses.

Pour cela nous allons créer une fonction que nous nommerons createJauge à laquelle nous passerons en paramètre la référence à l'objet « progress-circle » devant être initialisé.

function createJauge(elem) {
  if (elem) {
    // on commence par un clear
    while (elem.firstChild) {
      elem.removeChild(elem.firstChild);
    }
    // création des éléments
    var oMask  = document.createElement('DIV');
    var oBarre = document.createElement('DIV');
    var oSup50 = document.createElement('DIV');
    // affectation des classes
    oMask.className  = 'progress-masque';
    oBarre.className = 'progress-barre';
    oSup50.className = 'progress-sup50';
    // construction de l'arbre
    oMask.appendChild(oBarre);
    oMask.appendChild(oSup50);
    elem.appendChild(oMask);
  }
  return elem;
}

Il ne reste plus qu'à initialiser les éléments « progress-circle » de la page une fois le DOM chargé.

// Initialisation après chargement du DOM
document.addEventListener('DOMContentLoaded', function() {
    var oJauges = document.querySelectorAll('.progress-circle');
    var i, nb = oJauges.length;
    for( i=0; i < nb; i +=1){
      createJauge(oJauges[i]);
    }
});

Suppression du fichier CSS

L'utilisation du javascript permet également de ne pas avoir de fichier CSS pour toutes les valeurs de rotation.

Pour appliquer la rotation directement sur l'élément « progress-barre », nous allons créer une fonction que nous nommerons initJauge à laquelle nous passerons en paramètre la référence à l'objet « progress-circle » devant être initialisé.

function initJauge(elem) {
  var oBarre;
  var angle;
  var valeur;
  //
  createJauge( elem);
  oBarre = elem.querySelector('.progress-barre');
  valeur = elem.getAttribute('data-value');
  valeur = valeur ? valeur * 1 : 0;
  elem.setAttribute('data-value', valeur.toFixed(1));
  angle = 360 * valeur / 100;
  if (oBarre) {
    oBarre.style.transform = 'rotate(' + angle + 'deg)';
  }
}

Il ne reste plus qu'à initialiser les éléments « progress-circle » de la page une fois le DOM chargé.

// Initialisation après chargement du DOM
document.addEventListener('DOMContentLoaded', function () {
  var oJauges = document.querySelectorAll('.progress-circle');
  var i, nb = oJauges.length;
  for (i = 0; i < nb; i += 1) {
    initJauge(oJauges[i]);
  }
});

Ressources