[CSS-JS]Animation d'une progress-bar

NoSmoking

Nous allons voir comment animer des « progress-bars » à l'aide du CSS ou sur base d'une animation gérée en JavaScript.

Préambule

Les exemples présentent la structure suivante :

<div class="progress-group">
  <button class="btn-action">&#9654;</button>
  <button class="btn-reset">&#10006;&#xFE0E;</button>
  <div class="progressbar"></div>
</div>

Les « progress-bars » ont un style commun :

.progressbar {
  height: 2em;
  text-align: center;
  line-height: 2;
  background-repeat: no-repeat;
  background-size: 0;
}

Dans les exemples qui suivent, les boutons lance l'animation et les boutons « reset » celle-ci en utiliant le code basique suivant :

elemBoutonAction.addEventListener("click", () => {
  elemProgress.classList.add("animation");
});
elemBoutonReset.addEventListener("click", () => {
  elemProgress.classList.remove("animation");
});

On ajoute ou supprime la classe animation à l'élément « progress-bars ».

L'animation porte sur la propriété background-size qui passe de la valeur 0 à 100%.

CSS avec une transition

L'animation est gérée par le CSS en utilisant une transition.

CSS appliqué :

.progress-trans {
  border: 1px solid #ACC;
  background-image: linear-gradient(to right, #DFF, #5AA 100%);
}
.progress-trans.animation {
  background-size: 100%;
  transition: background-size var(--delay) linear;
}

On notera que la déclaration de la transition est faite au niveau du sélecteur possédant la classe animation ceci afin que le « reset » soit instantané.

CSS avec animation et @keyframes

Exemple 1

L'animation est gérée par le CSS en utilisant la propriété animation et une règle @keyframes.

CSS appliqué :

.progress-ex1.animation {
  animation: anim-progress-ex1 linear var(--delay) forwards;
}
@keyframes anim-progress-ex1 {
  0% {
    background-size: 0;
  }
  100% {
    background-size: 100%;
  }
}

L'avantage de passer par une animation est qu'un événement animationend est déclenché quand une animation CSS est terminée. Ceci permet de lancer une action à l'issue de celle-ci.

En fait on peut également faire la même chose avec les transitions en utilisant un écouteur sur l'événement transitionend à la place de animationend.

Dans l'exemple ci-dessus on modifie le contenu de la barre de progression en fin d'animation.

elemProgress.addEventListener("animationend", (event) => {
  const elem = event.target;
  elem.textContent = `Animation terminée après ${event.elapsedTime}s`;
});

Exemple 2

L'animation est gérée entièrement par le CSS en utilisant la propriété animation et une règle @keyframes mais cette fois ci on utilise un pseudo-élément ::before ce qui va nous permettre d'afficher l'encours de la progression.

CSS appliqué :

.progress-ex2:before {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  background-image: linear-gradient(to right, #FDE, #978 100%);
  background-repeat: no-repeat;
  background-size: 0;
}
.progress-ex2.animation::before {
  animation: anim-progress-ex2 linear var(--delay) forwards;
}

Les déclarations concernant les étapes de l'animation peuvent être aisément générées par JavaScript.


Les pseudo-élément ne font pas partie du DOM et à ce titre on ne peut pas les cibler en JavaScript.
Dans le cas ci-dessus la modification du texte fera donc partie du CSS, via le content du pseudo-élément mais l'on peut aussi choisir de changer la classe CSS de l'élément conteneur en fin d'animation.

Exemple 3

Exemple en remplaçant la classe animation par la classe animationend.

CSS appliqué :

.animationend {
  border-color: #800;
  color: #A00;
  background-color: #FDE;
  box-shadow: 0 0 1em #F00 inset;
}
.animationend:before {
  content: "End of animation";
}

JavaScript appliqué :

elemProgress.addEventListener("animationend", (event) => {
  const elem = event.target;
  elem.classList.replace("animation", "animationend");
});

JS avec requestAnimationFrame

L'animation est gérée par le JavaScript en utilisant la méthode requestAnimationFrame.

La fonction de contrôle de l'animation :

function startAnimation(element, duration) {
  let startTime;

  function repeatAnimation(timestamp) {
    if (undefined === startTime) {
      startTime = timestamp;
    }
    const progress = (timestamp - startTime);
    const pourcent = progress / duration * 100;

    if (progress < duration) {
      element.textContent = pourcent.toFixed(0) + "%";
      element.style.backgroundSize = pourcent + "%";
      element.requestID = window.requestAnimationFrame(repeatAnimation);
    }
    else {
      element.textContent = "100%";
      element.style.backgroundSize = "100%";
    }
  }
  window.cancelAnimationFrame(element.requestID);
  element.requestID = window.requestAnimationFrame(repeatAnimation);
}

L'avantage de cette approche est que l'on peut associer des fonctions à exécuter aux différentes étapes de l'animation, celles-ci peuvent même être passées à la fonction d'animation.

La structure HTML :

<div class="progress-group">
  <div class="progressbar progress-yellow" data-value="61"></div>
  <div class="progressbar progress-green" data-value="87"></div>
  <div class="progressbar progress-red" data-value="43"></div>
</div>

Les valeurs à atteindre sont mises dans l'attribut data-value de chaque élément.

Exemple de fonction :

function startAnimation(element, duration, onprogress, oncomplete) {
  let startTime;

  function repeatAnimation(timestamp) {
    if (undefined === startTime) {
      startTime = timestamp;
    }
    const progress = (timestamp - startTime);
    const pourcent = progress / duration * 100;

    if (progress < duration) {
      onprogress && onprogress(element, pourcent);
      element.requestID = window.requestAnimationFrame(repeatAnimation);
    }
    else {
      oncomplete && oncomplete(element, 100);
    }
  }
  window.cancelAnimationFrame(element.requestID);
  element.requestID = window.requestAnimationFrame(repeatAnimation);
}

Avec les fonctions appelées :

function onprogress(element, pourcent) {
  const max = element.dataset.value || 100;
  const inc = max / 100;
  const value = inc * pourcent;
  element.textContent = value.toFixed(0) + "%";
  element.style.backgroundSize = value + "%";
}

function oncomplete(element, pourcent) {
  onprogress(element, 100);
}

Et la fonction de lancement :

function lanceAnimation() {
  const duration = 3000;
  const elements = document.querySelectorAll(".progressbar");
  elements.forEach((el) => {
    startAnimation(el, duration, onprogress, oncomplete);
  });
}

Observations

On aurait pu également utiliser un élément HTML <progress>, le résultat aurait été le même.

L'aspect purement cosmétique de la « progress-bar » est une histoire de goût.

En matière de code pur l'utilisation du CSS est plus concise, il suffit d'un clic pour ajouter une classe à la « progress-bar » ou d'un déclenchement à retardement via la propriété transition-delay.

Le JavaScript peut quant à lui permettre de réaliser des choses plus abouties.

La solution à adopter sera donc fonction du besoin.

Ressources