Dans ce tutoriel je vous propose de découvrir comment créer un carrousel en utilisant le minimum de JavaScript possible.
Le problème
Pour présenter les contenus de notre site nous avons une grille qui contient l'ensemble de nos éléments.
.items {
display: grid;
gap: 1rem;
grid-template-columns: repeat(4, 1fr);
}
Pour gérer le responsive le premier réflexe serait de mettre un grid-template-columns: 1fr
pour empiler les éléments les uns sous les autres. Le problème est alors que la liste de nos élément va prendre beaucoup de place en hauteur. On souhaite plutôt créer un carrousel horizontal pour faire défiler les éléments.
La disposition mobile
La première étape est de changer la disposition sur mobile en utilisant un flex
au lieu de grid
et en imposant une largeur minimale à nos cartes.
Pour l'effet carrousel, on utilisera le système de scroll-snap qui permet de définir des points d'accroche lors du défilement du conteneur.
@media (max-width: 1000px) {
.items {
display: flex;
overflow-x: auto;
/* Pour coller au bord de l'écran on applique une marge négative */
padding: 1rem;
margin: -1rem;
/* On aimante le défilement */
scroll-snap-type: x proximity;
}
.item {
/* Permet d'"aimanter" l'élément quand on défile */
scroll-snap-align: center;
min-width: 250px;
}
}
Et c'est tout ! Avec ces quelques lignes on obtient un comportement de carrousel géré par les navigateur avec un système d'accroche qui va centrer automatiquement l'élément visible à l'écran.
Un carrousel pour les grands écrans
On souhaite maintenant mettre en place une disposition similaire sur les grands écran en remplaçant notre grille par un système de défilement horizontal. Dans ce cas là on peut aussi utiliser le système de flex comme disposition de base.
.items {
display: flex;
--items: 4;
--gap: 1rem;
gap: var(--gap);
overflow-x: auto;
scroll-snap-type: x proximity;
/* Pour ne pas couper les éléments visuellement, on utilise un padding */
padding: calc(var(--gap) * .5);
margin: calc(var(--gap) * -.5);
scroll-padding-inline: calc(var(--gap) * .5);
}
.item {
min-width: calc((100% - (var(--items) - 1) * var(--gap)) / var(--items));
scroll-snap-align: start;
}
/* On masque les scrollbars quand une souris est utilisée pour n'utiliser que les boutons de navigation */
@media (min-width: 1001px) and (pointer: fine) {
.items {
overflow: hidden;
}
}
@media (max-width: 1000px) {
.items {
padding: 1rem;
margin: -1rem;
scroll-padding-inline: 0;
}
.item {
scroll-snap-align: center;
min-width: 250px;
}
}
On souhaite aussi pouvoir naviguer avec des boutons suivant et précédent. Pour cette navigation on devra rajouter du JavaScript pour piloter le défilement et on va commencer par marquer les éléments clefs à l'aide d'attribut data-*
. On ne changera pas grand chose à notre structure HTML car on souhaite continuer à utiliser le système de défilement natif pour piloter notre carrousel.
<div class="relative" data-slider>
<div class="items" data-slider-wrapper>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
<article class="item"><!-- Contenu --></article>
</div>
<button data-slider-next>Suivant</button>
<button data-slider-prev hidden>Précédent</button>
</div>
Côté JavaScript on va se contenter de détecter les clics sur les boutons pour faire défiler notre élément data-slider-wrapper
.
class Slider {
/**
* @param {HTMLDivElement} el
*/
constructor(el) {
this.nextButton = el.querySelector('[data-slider-next]')
this.prevButton = el.querySelector('[data-slider-prev]')
this.wrapper = el.querySelector('[data-slider-wrapper]')
this.nextButton.addEventListener('click', () => this.move(1))
this.prevButton.addEventListener('click', () => this.move(-1))
this.wrapper.addEventListener('scrollend', () => this.updateUI())
this.updateUI()
}
/**
* Utilise la variable --items pour déterminer le nombre d'élément visible
**/
get itemsToScroll () {
return parseInt(window.getComputedStyle(this.wrapper).getPropertyValue('--items'), 10);
}
/**
* Nombre total de "pages" dans notre carrousel
* @returns {number}
**/
get pages () {
return Math.ceil(this.wrapper.children.length / this.itemsToScroll)
}
/**
* Page courante
* @returns {number}
**/
get page () {
return Math.ceil(this.wrapper.scrollLeft / this.wrapper.offsetWidth)
}
/**
* Affiche / Masque les boutons de navigation
**/
updateUI () {
if (this.page === 0) {
this.prevButton.setAttribute('hidden', 'hidden')
} else {
this.prevButton.removeAttribute('hidden')
}
if (this.page === this.pages - 1) {
this.nextButton.setAttribute('hidden', 'hidden')
} else {
this.nextButton.removeAttribute('hidden')
}
}
/**
* Déplace le carousel de n pages
* @param {number} n
*/
move (n) {
let newPage = this.page + n
if (newPage < 0) {
newPage = 0;
}
if (newPage >= this.pages) {
newPage = this.pages - 1
}
this.wrapper.scrollTo({
left: this.wrapper.children[newPage * this.itemsToScroll].offsetLeft,
behavior: 'smooth'
})
}
}
// On branche notre comportement à tous les éléments
Array.from(document.querySelectorAll("[data-slider]"))
.map(el => new Slider(el))
On pourra ensuite masquer les boutons de navigation sur mobile vu qu'ils ne sont pas nécessaire (les utilisateurs préfèrent faire défiler via des mouvements).
@media (max-width: 1000px) {
.rooms_navigation {
display: none;
}
}
Avec ce code on obtient un comportement de carrousel avec un minimum de code.