French (Français) translation by fredpiK (you can also view the original English article)
Dans un précédent article, je vous ai montré comment réaliser une Timeline verticale responsive en partant de zéro. Aujourd'hui, je vais couvrir le processus de création de la Timeline horizontale associée.
Comme d'habitude, pour vous faire une idée initiale de ce que nous réaliserons, jetez un coup d'oeil à la démo CodePen (choisissez la version élargie pour un meilleur rendu)
Nous avons beaucoup de choses à voir, alors commençons!
1. Balisage HTML
Le balisage est identique à celui que nous avons défini pour la Timeline verticale, hormis trois petites choses:
- Nous utilisons une liste ordonnée plutôt que non-ordonnée car c'est sémantiquement plus correct.
- Il y a un élément de liste supplémentaire (le dernier) qui est vide. Dans une section à venir, nous expliquerons pourquoi.
- Il y a un élément supplémentaire (c-à-d
.arrows
) qui est responsable de la navigation dans la Timeline.
Voici le balisage requis:
<section class="timeline"> <ol> <li> <div> <time>1934</time> Some content here </div> </li> <!-- more list items here --> <li></li> </ol> <div class="arrows"> <button class="arrow arrow__prev disabled" disabled> <img src="arrow_prev.svg" alt="prev timeline arrow"> </button> <button class="arrow arrow__next"> <img src="arrow_next.svg" alt="next timeline arrow"> </button> </div> </section>
L'état initial de la Timeline ressemble à ça:
2. Ajout des Styles CSS Initiaux
Après les styles de base pour les polices, les couleurs, etc. que j'ai omis pour rester simple, nous spécifions quelques régles CSS structurelles.
.timeline { white-space: nowrap; overflow-x: hidden; } .timeline ol { font-size: 0; width: 100vw; padding: 250px 0; transition: all 1s; } .timeline ol li { position: relative; display: inline-block; list-style-type: none; width: 160px; height: 3px; background: #fff; } .timeline ol li:last-child { width: 280px; } .timeline ol li:not(:first-child) { margin-left: 14px; } .timeline ol li:not(:last-child)::after { content: ''; position: absolute; top: 50%; left: calc(100% + 1px); bottom: 0; width: 12px; height: 12px; transform: translateY(-50%); border-radius: 50%; background: #F45B69; }
Plus important ici, vous remarquerez deux choses:
- Nous ajoutons de grands paddings en haut et en bas de la liste. Là encore, nous expliquerons pourquoi dans une prochaine section.
- Comme vous pouvez le constater dans la démo suivante, à ce stade, nous ne pouvons pas voir tous les éléments de liste car la liste a
width: 100vw
et son parent aoverflow-x: hidden
. Cela masque effectivement les éléments de liste. Grâce à la navigation de la Timeline, néanmoins, nous pourrons naviguer dans ces éléments plus tard.
Une fois ces règles en place, voici l'état actuel de la Timeline (sans aucun contenu, pour rester clair):
3. Styles des éléments de la Timeline
A ce stade, nous allons définir les styles des éléments div
(nous les appellerons "Eléments de Timeline" à partir de maintenant) qui font partie des éléments de liste ainsi que leur pseudo-élément ::before
.
En plus de cela, nous allons utiliser les pseudo-classes CSS :nth-child(odd)
et :nth-child(even)
pour différencier les styles des divs impaires et paires.
Voici les styles communs aux éléments de Timeline.
.timeline ol li div { position: absolute; left: calc(100% + 7px); width: 280px; padding: 15px; font-size: 1rem; white-space: normal; color: black; background: white; } .timeline ol li div::before { content: ''; position: absolute; top: 100%; left: 0; width: 0; height: 0; border-style: solid; }
Puis les styles pour les impairs:
.timeline ol li:nth-child(odd) div { top: -16px; transform: translateY(-100%); } .timeline ol li:nth-child(odd) div::before { top: 100%; border-width: 8px 8px 0 0; border-color: white transparent transparent transparent; }
Et enfin les styles pour les pairs:
.timeline ol li:nth-child(even) div { top: calc(100% + 16px); } .timeline ol li:nth-child(even) div::before { top: -8px; border-width: 8px 0 0 8px; border-color: transparent transparent transparent white; }
Voici le nouvel état de la Timeline, avec le contenu remis en place:
Comme vous l'avez probablement remarqué, les Eléments de Timeline sont positionnés en absolu. Cela signifie qu'ils sont exclus du flux normal du document. Sachant cela, pour s'assurer que l'intégralité de la Timeline s'affiche, nous devons définir des valeurs importantes de padding en haut et en bas de la liste. Si nous n'ajoutons pas de padding, la Timeline sera rognée:

4. Styles de la Navigation dans la Timeline
Il est temps maintenant de styler les boutons de navigation. Rappelez-vous que par défaut, nous avons désactivé la flèche 'Précédent' et lui avons assigné la classe disabled
.
Voici les styles CSS associés:
.timeline .arrows { display: flex; justify-content: center; margin-bottom: 20px; } .timeline .arrows .arrow__prev { margin-right: 20px; } .timeline .disabled { opacity: .5; } .timeline .arrows img { width: 45px; height: 45px; }
Les règles ci-dessus nous donnent ce résultat:
5. Ajout d’interactivité
La structure de base de la Timeline est prête. Ajoutons-lui un peu d'interactivité!
Variables
Premièrement, nous définissons plusieurs variables qui nous seront utiles plus tard.
const timeline = document.querySelector(".timeline ol"), elH = document.querySelectorAll(".timeline li > div"), arrows = document.querySelectorAll(".timeline .arrows .arrow"), arrowPrev = document.querySelector(".timeline .arrows .arrow__prev"), arrowNext = document.querySelector(".timeline .arrows .arrow__next"), firstItem = document.querySelector(".timeline li:first-child"), lastItem = document.querySelector(".timeline li:last-child"), xScrolling = 280, disabledClass = "disabled";
Initialisation
Lorsque tous les attributs de la page sont prêts, la fonction init
est appelée.
window.addEventListener("load", init);
Cette fonction déclenche quatre sous-fonctions:
function init() { setEqualHeights(elH); animateTl(xScrolling, arrows, timeline); setSwipeFn(timeline, arrowPrev, arrowNext); setKeyboardFn(arrowPrev, arrowNext); }
Comme nous le verrons dans un moment, chacune de ces fonctions accomplit une tâche précise.
Eléments de Timeline de hauteur égale
Si vous revenez à la dernière démo, vous remarquerez que les éléments de Timeline n'ont pas la même hauteur. Cela n'affecte pas le fonctionnement de notre Timeline, mais vous pourriez préférer que tous les éléments soient de même hauteur. Pour accomplir ceci, nous pouvons leur donner soit une hauteur fixe via CSS (solution facile), soit une hauteur dynamique correspondant à celle du plus grand élément via Javascript.
La seconde solution est plus flexible et stable, donc voici une fonction qui implémente ce comportement:
function setEqualHeights(el) { let counter = 0; for (let i = 0; i < el.length; i++) { const singleHeight = el[i].offsetHeight; if (counter < singleHeight) { counter = singleHeight; } } for (let i = 0; i < el.length; i++) { el[i].style.height = `${counter}px`; } }
Cette fonction récupère la hauteur du plus grand élément de Timeline et la définit comme hauteur par défaut pour tous les éléments.
Voici à quoi ressemble la démo:
6. Animer la Timeline
Concentrons-nous maintenant sur l'animation de la Timeline. Nous allons construire pas-à-pas la fonction qui implémente ce comportement
En premier lieu, nous enregistrons un écouteur d'évènement de clic pour les boutons de la Timeline.
function animateTl(scrolling, el, tl) { for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // code here }); } }
Chaque fois qu'un bouton est cliqué, nous vérifions l'état inactif des boutons de la Timeline et s'ils ne sont pas désactivés, nous les désactivons. Ceci garantit que les deux boutons seront cliqués une fois seulement avant que l'animation ne se termine.
Donc, en terme de code, le gestionnaire de clic contient initiallement les lignes suivantes:
if (!arrowPrev.disabled) { arrowPrev.disabled = true; } if (!arrowNext.disabled) { arrowNext.disabled = true; }
Les étapes suivantes se déroulent comme suit:
- Nous vérifions si c'est la première fois que nous avons cliqué sur un bouton. Là encore, gardez à l'esprit que le bouton Précédent est désactivé par défaut, donc le seul bouton qui puisse être cliqué au démarrage est le bouton Suivant.
- Si c'est effectivement la première fois, nous utilisons la propriété
transform
pour déplacer la Timeline de 280px vers la droite. La valeur de la variablexScrolling
définit la quantité de mouvement. - Au contraire, si nous avons déjà cliqué sur un bouton, nous récupérons la valeur actuelle de
transform
de la Timeline et ajoutons ou retranchons à cette valeur la quantité de mouvement voulue (c.à.d 280px). Donc, tant que nous cliquons sur le bouton Précédent, la valeur de la propriététransform
diminue et la Timeline est déplacée de gauche à droite. Cependant, quand le bouton Suivant est cliqué, la valeur de la propriététransform
augmente et la Timeline est déplacée de droite à gauche.
Le code qui implémente cette fonctionnalité est comme suit:
let counter = 0; for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here const sign = (this.classList.contains("arrow__prev")) ? "" : "-"; if (counter === 0) { tl.style.transform = `translateX(-${scrolling}px)`; } else { const tlStyle = getComputedStyle(tl); // add more browser prefixes if needed here const tlTransform = tlStyle.getPropertyValue("-webkit-transform") || tlStyle.getPropertyValue("transform"); const values = parseInt(tlTransform.split(",")[4]) + parseInt(`${sign}${scrolling}`); tl.style.transform = `translateX(${values}px)`; } counter++; }); }
Beau boulot! Nous venons de définir un moyen d'animer la Timeline. Le prochain défi sera de déterminer quand cette animation doit cesser. Voici notre approche:
- Quand le premier élément de Timeline est entièrement visible, cela signifie que nous avons déjà atteint le début de la Timeline, et donc nous désactivons le bouton Précédent. Nous vérifions aussi que le bouton Suivant est actif.
- Quand le dernier élément de Timeline devient entièrement visible, cela signifie que nous nous avons déjà atteint la fin de la Timeline, et donc nous désactivons le bouton Suivant. Nous vérifions donc aussi que le bouton Précédent est actif.
Rappelez-vous que le dernier élément est un élément vide de largeur égale à celle des éléments de Timeline (c.à.d 280px). Nous lui affectons cette valeur (ou une plus grande) car nous voulons être sûrs que le dernier élément sera visible avant de désactiver le bouton Suivant.
Pour détecter si les éléments cible sont entièrement visibles dans la vue courante ou pas, nous tirerons parti du même code que nous avons utilisé pour la Timeline verticale. Le code nécessaire issu de ce fil Stack Overflow s'écrit comme suit:
function isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }
En plus de la fonction ci-dessus, nous définissons un autre assistant:
function setBtnState(el, flag = true) { if (flag) { el.classList.add(disabledClass); } else { if (el.classList.contains(disabledClass)) { el.classList.remove(disabledClass); } el.disabled = false; } }
Cette fonction ajoute ou enlève la classe disabled
d'un élément en fonction de la valeur du paramètre flag
. Additionnellement, il peut modifier l'état actif de cet élément.
Etant donné ce que nous avons décrit au-dessus, voici le code que nous définissons pour tester si l'animation doit stopper ou pas.
for (let i = 0; i < el.length; i++) { el[i].addEventListener("click", function() { // other code here // code for stopping the animation setTimeout(() => { isElementInViewport(firstItem) ? setBtnState(arrowPrev) : setBtnState(arrowPrev, false); isElementInViewport(lastItem) ? setBtnState(arrowNext) : setBtnState(arrowNext, false); }, 1100); // other code here }); }
Remarquez qu'il y a un délai de 1.1 seconde avant que ce code ne s'éxécute. Pourquoi cela se produit t-il?
Si nous revenons à notre CSS, nous verrons cette règle:
.timeline ol { transition: all 1s; }
Ainsi, l'animation de la Timeline prend 1 seconde pour s'effectuer. Lorsque elle se termine, nous attendons 100 millisecondes et alors nous effectuons nos tests.
Voici la Timeline avec les animations:
7. Ajout du Support du Swipe (glisser)
Jusque là, la Timeline ne réagit pas aux événements de touche. Ce serait quand même bien si nous pouvions ajouter cette fonctionnalité. Pour réaliser ceci, nous pouvons écrire notre propre implémentation Javascript ou utiliser une des bibliothèques apparentées (par ex. Hammer.js, TouchSwipe.js) qui existent.
Pour notre démo, nous allons faire simple et utiliser Hammer.js, donc, tout d'abord, nous incluons la bibliothèque dans notre Pen:

Puis nous déclarons la fonction associée:
function setSwipeFn(tl, prev, next) { const hammer = new Hammer(tl); hammer.on("swipeleft", () => next.click()); hammer.on("swiperight", () => prev.click()); }
A l'intérieur de la fonction ci-dessus, nous faisons comme suit:
- Création d'une instance Hammer.
- Enregistrement des gestionnaires d'évènements
swipeleft
etswiperight
. - Quand nous faisons glisser la Timeline vers la gauche, nous déclenchons un clic sur le bouton Suivant, et ainsi la Timeline est animée de droite à gauche.
- Quand nous faisons glisser la Timeline vers la droite, nous déclenchons un clic sur le bouton Précédent, et ainsi la Timeline est animée de gauche à droite.
La Timeline avec la gestion du glisser:
Ajout de la navigation au clavier
Améliorons encore l'expérience utilisateur en permettant la navigation au clavier. Nos objectifs:
- Lorsque la flèche gauche ou droite est enfoncée, le document doit être déplacé vers la position haute de la Timeline (si une autre section de la page est actuellement visible). Cela garantit que toute la Timeline sera visible.
- Plus spécifiquement, quand la flèche gauche est enfoncée, la Timeline doit être animée de gauche à droite.
- De la même façon, lorsque la flèche droite est pressée, la Timeline doit être animée de droite à gauche.
La fonction associée est comme suit:
function setKeyboardFn(prev, next) { document.addEventListener("keydown", (e) => { if ((e.which === 37) || (e.which === 39)) { const timelineOfTop = timeline.offsetTop; const y = window.pageYOffset; if (timelineOfTop !== y) { window.scrollTo(0, timelineOfTop); } if (e.which === 37) { prev.click(); } else if (e.which === 39) { next.click(); } } }); }
La Timeline avec le support du clavier:
8. En Responsive
Nous y somme presque! Dernière chose, mais pas des moindres, nous allons rendre la Timeline responsive. Quand la vue fait moins de 600px, elle doit avoir la structure empilée suivante:

Comme nous utilisons une approche "Desktop-first" (pour Ordinateur de bureau d'abord), voici les règles CSS que nous devons réécrire:
@media screen and (max-width: 599px) { .timeline ol, .timeline ol li { width: auto; } .timeline ol { padding: 0; transform: none !important; } .timeline ol li { display: block; height: auto; background: transparent; } .timeline ol li:first-child { margin-top: 25px; } .timeline ol li:not(:first-child) { margin-left: auto; } .timeline ol li div { width: 94%; height: auto !important; margin: 0 auto 25px; } .timeline ol li:nth-child div { position: static; } .timeline ol li:nth-child(odd) div { transform: none; } .timeline ol li:nth-child(odd) div::before, .timeline ol li:nth-child(even) div::before { left: 50%; top: 100%; transform: translateX(-50%); border: none; border-left: 1px solid white; height: 25px; } .timeline ol li:last-child, .timeline ol li:nth-last-child(2) div::before, .timeline ol li:not(:last-child)::after, .timeline .arrows { display: none; } }
Remarque: Pour deux des règles ci-dessus, nous avons dû utiliser la règle !important
pour écraser les styles inline associés appliqués par Javascript.
L'état final de notre Timeline:
Support Navigateur
La démo fonctionne bien sur tous les appareils et navigateurs récents. De plus, comme vous l'avez peut-être remarqué, nous utilisons Babel pour compiler notre code ES6 en ES5.
Le seul petit souci que j'ai rencontré pendant les tests est le changement de rendu du texte quand la Timeline est animée. Bien que j'aie essayé plusieurs des approches proposées dans les différents fils Stack Overflow, je n'ai pas trouvé de solution toute faite pour tous les systèmes d'exploitation et navigateurs. Aussi, gardez à l'esprit que vous pourriez de légers problèmes d'affichage des polices lorsque la Timeline est animée.
Conclusion
Dans ce tutoriel assez conséquent, nous avons commencé avec une simple liste ordonnée et créé une Timeline horizontale responsive. Sans doute, nous avons couvert beaucoup de choses intéressantes, mais j'espère que vous avez aimé travailler jusqu'au résultat final et que cela vous a aidé à acquérir de nouveaux savoirs.
Si vous avez la moindre question ou si vous n'avez pas compris quelque chose, faites-le moi savoir dans les commentaires ci-dessous!
Etapes suivantes
Si vous voulez encore améliorer ou enrichir cette Timeline, voici quelques choses que vous pourriez faire:
- Ajouter le support pour le Dragging. Plutôt que de cliquer sur les boutons de la Timeline, nous pourrions simplement faire glisser la zone de la Timeline. Pour cela, vous pourriez utiliser l'API Drag and Drop native (qui ne supporte malheureusement pas les appareils mobiles à l'heure où j'écris) ou une bibliothèque externe telle que Draggable.js.
- Améliorer le comportement de la Timeline au redimensionnement de la fenêtre du navigateur. Par exemple, quand nous redimensionnons la fenêtre, les boutons devront être activés et désactivés en conséquence.
- Organiser le code de manière plus facile à gérer. Peut-être en utilisant un Design Pattern Javascript populaire.
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post