ui · carousel

Carousel

Défilement de slides avec flèches et puces de navigation. Utilise le scroll-snap natif du navigateur (fluide, tactile sur mobile), piloté par un peu de JavaScript pour les flèches/puces.

Voir le code source
<style>
.bn-carousel {
  position: relative;
  font-family: 'Inter', sans-serif;
  background: #0B0F10;
  padding: 1rem;
}

.bn-carousel__track {
  display: flex;
  overflow-x: auto;
  /* Le scroll-snap natif fait le plus gros du travail : chaque
     slide "s'accroche" proprement au bord au lieu de s'arrêter
     n'importe où pendant le défilement */
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  border-radius: 6px;
}

.bn-carousel__track::-webkit-scrollbar {
  display: none;
}

.bn-carousel__slide {
  flex: 0 0 100%;
  scroll-snap-align: start;

  height: 260px;
  display: flex;
  align-items: center;
  justify-content: center;

  background: linear-gradient(135deg, #1D2324, #0B0F10);
  color: #E6E4DD;
  font-family: 'Bebas Neue', sans-serif;
  font-size: 2rem;
}

.bn-carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);

  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: 1px solid rgba(230, 228, 221, 0.3);
  background: rgba(5, 8, 8, 0.6);
  color: #E6E4DD;
  cursor: pointer;
}

.bn-carousel__arrow:hover {
  border-color: #E6E4DD;
}

.bn-carousel__arrow--prev { left: 1.5rem; }
.bn-carousel__arrow--next { right: 1.5rem; }

.bn-carousel__dots {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 1rem;
}

.bn-carousel__dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: none;
  background: rgba(230, 228, 221, 0.25);
  cursor: pointer;
  transition: background 0.2s ease, transform 0.2s ease;
}

.bn-carousel__dot.is-active {
  background: #E6E4DD;
  transform: scale(1.3);
}

</style>

<div class="bn-carousel" data-fx-root>
  <div class="bn-carousel__track">
    <div class="bn-carousel__slide"><span>1</span></div>
    <div class="bn-carousel__slide"><span>2</span></div>
    <div class="bn-carousel__slide"><span>3</span></div>
    <div class="bn-carousel__slide"><span>4</span></div>
  </div>

  <button type="button" class="bn-carousel__arrow bn-carousel__arrow--prev" aria-label="Précédent">←</button>
  <button type="button" class="bn-carousel__arrow bn-carousel__arrow--next" aria-label="Suivant">→</button>

  <div class="bn-carousel__dots">
    <button type="button" class="bn-carousel__dot is-active" data-index="0" aria-label="Aller à la slide 1"></button>
    <button type="button" class="bn-carousel__dot" data-index="1" aria-label="Aller à la slide 2"></button>
    <button type="button" class="bn-carousel__dot" data-index="2" aria-label="Aller à la slide 3"></button>
    <button type="button" class="bn-carousel__dot" data-index="3" aria-label="Aller à la slide 4"></button>
  </div>
</div>


<script>
/* ==========================================================
   CAROUSEL
   Les flèches font défiler d'une slide à la fois. Les puces
   sautent directement à la slide voulue. La puce active se met à
   jour automatiquement en fonction de la position du scroll — même
   si l'utilisateur fait défiler à la souris ou au doigt sans
   toucher aux boutons.
   ========================================================== */

(function () {
  const root = document.querySelector('.bn-carousel[data-fx-root]');
  if (!root) return;

  const track = root.querySelector('.bn-carousel__track');
  const slides = root.querySelectorAll('.bn-carousel__slide');
  const dots = root.querySelectorAll('.bn-carousel__dot');
  const prevBtn = root.querySelector('.bn-carousel__arrow--prev');
  const nextBtn = root.querySelector('.bn-carousel__arrow--next');

  function goTo(index) {
    const clamped = Math.max(0, Math.min(index, slides.length - 1));
    track.scrollTo({ left: slides[clamped].offsetLeft, behavior: 'smooth' });
  }

  function currentIndex() {
    return Math.round(track.scrollLeft / track.clientWidth);
  }

  prevBtn.addEventListener('click', () => goTo(currentIndex() - 1));
  nextBtn.addEventListener('click', () => goTo(currentIndex() + 1));

  dots.forEach((dot) => {
    dot.addEventListener('click', () => goTo(Number(dot.dataset.index)));
  });

  // Met à jour la puce active pendant le scroll (avec un léger
  // throttle via requestAnimationFrame pour rester fluide)
  let ticking = false;
  track.addEventListener('scroll', () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const index = currentIndex();
      dots.forEach((dot, i) => dot.classList.toggle('is-active', i === index));
      ticking = false;
    });
  });
})();

</script>