Tutoriel Vidéo JavaScript Loader animé

Télécharger la vidéo Voir la démo

Bienvenue dans cette vidéo où je vous propose de découvrir comment créer un loader animé en utilisant du JavaScript. Pour créer ce module nous allons utiliser l’API des éléments personnalisés afin de créer un nouvel élément que l'on va pouvoir réutiliser.

Pour ce loader nous allons essayer d'utiliser l'élément que l'on voit sur la maquette Estudio Mobile App UI Kit 1.

Un élément personnalisé ?

La création d'un élément personnalisé va permettre d'avoir un élément qui sera simple à utiliser et nous permettra aussi d'utiliser le Shadow DOM. Ce Shadow DOM nous permettra d'isoler complètement le style de notre loader afin qu'il ne déborde pas sur la page, et d'éviter que le style de la page déborde sur notre loader. Cela assure une meilleure isolation mais aussi un meilleur confort de travail pour des éléments indépendant.

class SpinningDots extends HTMLElement {
  constructor() {
    super()
    const styles = window.getComputedStyle(this)
    const width = this.intFromPx(styles.width, 28)
    const strokeWidth = this.intFromPx(styles.strokeWidth, (4 / 28) * width, 1)
    const circles = this.intFromPx(this.getAttribute('dots'), 8)
    const root = this.attachShadow({ mode: 'open' }) // Notre shadow DOM 
    root.innerHTML = `<div>
    ${this.buildStyles(width, circles, strokeWidth)}
    ${this.buildCircles(width, circles, strokeWidth / 2)}
    ${this.buildTrail(width, strokeWidth)}
    </div>`
  }

  // ...
}

Les petits points

Le premier élément qui compose notre loader est un ensemble de points disposés de manière circulaire et qui tournent lentement. Pour représenter ces différents éléments la solution la plus simple est l'utilisation d'un SVG.

buildCircles(w, n, r) {
  const circleRadius = (w / 2) - r
  let dom = `<svg class="circles" width="${w}" height="${w}" viewBox="0 0 ${w} ${w}" fill="none" xmlns="http://www.w3.org/2000/svg">`
  for (let i = 0; i < n; i++) {
    const a = (Math.PI / (n / 2)) * i
    const x = circleRadius * Math.sin(a) + w / 2
    const y = circleRadius * Math.cos(a) + w / 2
    dom += `<circle cx="${x}" cy="${y}" r="${r}" fill="currentColor"/>`
  }
  return dom + `</svg>`
}

Pour la partie animation, il nous suffira d'appliquer une règle CSS afin d'animer une rotation de 360° sur une durée définie.

.circles {
  animation: spin 16s linear infinite;
}
@keyframes spin {
    from {transform: rotate(0deg); }
    to {transform: rotate(360deg); }
}

La trainée

Le second élément qui compose ce loader est une traînée qui tourne aussi sur elle même. Pour réaliser cet effet il suffit de tracer un cercle à la bonne taille et de jouer sur les propriétés CSS stroke-dasharray et stroke-dashoffset afin d'obtenir un remplissage partiel du cercle.

buildTrail(w, stroke) {
  return `<svg class="trail" width="${w}" height="${w}" viewBox="0 0 ${w} ${w}" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="${w / 2}" cy="${w / 2}" r="${w / 2 -
    stroke / 2}" stroke-width="${stroke}" stroke-linecap="round" stroke="currentColor"/>
</svg>`
}
.trail {
  animation: spin2 1.6s cubic-bezier(.5,.15,.5,.85)  infinite;
} 
.trail circle {
  stroke-dasharray: PERIMETRE;
  stroke-dashoffset: PERIMETRE + 1 * SECTION;
  animation: trail 1.6s cubic-bezier(.5,.15,.5,.85)   infinite;
}
@keyframes spin2 {
    from {transform: rotate(0deg); }
    to {transform: rotate(720deg); }
}
@keyframes trail {
  0% { stroke-dashoffset: PERIMETRE + 1 * SECTION; }
  50% { stroke-dashoffset: PERIMETRE + 2.5 * SECTION; }
  100% { stroke-dashoffset: PERIMETRE + 1 * SECTION; }
}

Comme pour l'élément précédent nous allons ensuite appliquer une animation CSS afin d'effectuer une rotation complète de la traîner. On utilisera une courbe de bézier pour rendre l'effet d'accélération pour cette animation.

.trail {
  animation: spin2 1.6s cubic-bezier(.5,.15,.5,.85)  infinite;
}
@keyframes spin2 {
    from {transform: rotate(0deg); }
    to {transform: rotate(720deg); }
}

Maintenant, si on analyse de plus près l'animation originale, on remarque qu'avec l'augmentation de la vitesse la traînée s'allonge. Cela permet d'obtenir une animation plus fluide et de respecter un des 12 principes d'animation Squash and stretch (un élément doit se déformer pour signifier un mouvement). Pour gérer cet effet de déformation nous allons animer les propriétés stroke-dasharray et stroke-dashoffset en même temps que la rotation.

.trail {
  animation: spin2 1.6s cubic-bezier(.5,.15,.5,.85)  infinite;
} 
.trail circle {
  stroke-dasharray: PERIMETRE;
  stroke-dashoffset: PERIMETRE + 1 * SECTION;
  animation: trail 1.6s cubic-bezier(.5,.15,.5,.85)   infinite;
}
@keyframes spin2 {
    from {transform: rotate(0deg); }
    to {transform: rotate(720deg); }
}
@keyframes trail {
  0% { stroke-dashoffset: PERIMETRE + 1 * SECTION; }
  50% { stroke-dashoffset: PERIMETRE + 2.5 * SECTION; }
  100% { stroke-dashoffset: PERIMETRE + 1 * SECTION; }
}

Pour générer ce CSS, il faudra au préalable calculer le périmètre du cercle afin de calculer le dasharray et le dashoffset à appliquer. Ce code pourrait être simplifié gràce à l'utilisation de la propriété pathLength sur le SVG mais le support n'est pas encore optimale (la propriété est ignorée sur les version 12- de Safari et sur Edge).

Je veux le code !

Si vous voulez jeter un oeil sur le code final, ou juste utiliser l'élément j'ai publié l'élément sur github et npm.