Usually not. Modern CSS covers most galleries: scroll-snap for swipeable sliders, CSS Grid for masonry-style layouts, keyframed marquees for logo strips and simple lightboxes. A few lines of vanilla JavaScript add autoplay, thumbnails, zoom or before/after comparison without shipping a dependency.

How to choose, performance and accessibility
  • Choose CSS-Only Carousel or Basic Slider for product or portfolio images, a lightbox when people need the full-size photo, and Velocity Marquee for logo or testimonial bands.
  • Lazy-load images and give them explicit width and height, keep arrows and dots reachable by keyboard, and stop autoplay as soon as the user interacts or asks for reduced motion.
  • Basic Slider, Basic Lightbox, Hover Zoom, Basic Marquee, CSS-Only Carousel and Velocity Marquee are free with their code; the rest come with Premium.

Basic Slider

Simple horizontal slider with navigation arrows.

CSS Transform Arrows
View source code
HTML
<div class="demo-preview">
  <div class="carousel-container slider-basic" id="sliderBasic">
    <div class="carousel-track">
      <div class="carousel-slide">Slide 1</div>
      <div class="carousel-slide">Slide 2</div>
      <div class="carousel-slide">Slide 3</div>
      <div class="carousel-slide">Slide 4</div>
      <div class="carousel-slide">Slide 5</div>
    </div>
    <button class="carousel-arrow prev">
      <svg viewBox="0 0 24 24"><path d="M15 18l-6-6 6-6"/></svg>
    </button>
    <button class="carousel-arrow next">
      <svg viewBox="0 0 24 24"><path d="M9 18l6-6-6-6"/></svg>
    </button>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --bg-card: #12121a;
  --radius-xl: 1rem;
}

.carousel-container {
  width: 320px;
  height: 200px;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-xl);
  background: var(--bg-card);
}

.carousel-track {
  display: flex;
  height: 100%;
  transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.carousel-slide {
  min-width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 1.2rem;
  color: white;
}

.carousel-slide:nth-child(1) {
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}

.carousel-slide:nth-child(2) {
  background: linear-gradient(135deg, #8b5cf6, #d946ef);
}

.carousel-slide:nth-child(3) {
  background: linear-gradient(135deg, #d946ef, #f43f5e);
}

.carousel-slide:nth-child(4) {
  background: linear-gradient(135deg, #f43f5e, #f97316);
}

.carousel-slide:nth-child(5) {
  background: linear-gradient(135deg, #f97316, #eab308);
}

.carousel-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 36px;
  background: rgba(255, 255, 255, 0.2);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s ease;
  z-index: 10;
}

.carousel-arrow:hover {
  background: rgba(255, 255, 255, 0.4);
  transform: translateY(-50%) scale(1.1);
}

.carousel-arrow svg {
  width: 18px;
  height: 18px;
  stroke: white;
  stroke-width: 2;
  fill: none;
}

.carousel-arrow.prev {
  left: 10px;
}

.carousel-arrow.next {
  right: 10px;
}

.slider-basic .carousel-track {
  transition: transform 0.5s ease-out;
}
JavaScript
(function() {
  /* fx-0161-nav */
  /* Navigation du Basic Slider : les fleches etaient stylees mais
     cablees nulle part — ni ici, ni dans le code copie par le visiteur. */
  // Navigation du carrousel : le CSS anime .carousel-track, le JS pose la position.
  // querySelectorAll : plusieurs carrousels peuvent coexister sur une meme page.
  (function() {
    var roots = document.querySelectorAll('#sliderBasic, .slider-basic');
    Array.prototype.forEach.call(roots, function(root) {
      var track = root.querySelector('.carousel-track');
      var slides = root.querySelectorAll('.carousel-slide');
      var prev = root.querySelector('.carousel-arrow.prev');
      var next = root.querySelector('.carousel-arrow.next');
      if (!track || !slides.length) return;

      var index = 0;

      function aller(n) {
        index = (n + slides.length) % slides.length; // defilement circulaire
        track.style.transform = 'translateX(-' + (index * 100) + '%)';
      }
      if (prev) prev.addEventListener('click', function() {
        aller(index - 1);
      });
      if (next) next.addEventListener('click', function() {
        aller(index + 1);
      });

      // Fleches du clavier quand ce carrousel a le focus (sans gener la page hote)
      if (!root.hasAttribute('tabindex')) root.setAttribute('tabindex', '0');
      root.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft') {
          e.preventDefault();
          aller(index - 1);
        }
        if (e.key === 'ArrowRight') {
          e.preventDefault();
          aller(index + 1);
        }
      });

      aller(0);
    });
  })();
})();

Fade Slider

Fade transition between slides.

CSS Opacity Fade

Auto-play Slider

Auto-scrolling with progress bar.

JS Timer Progress

Dots Navigation

Circular dot indicator navigation.

CSS Dots Click

Thumbnail Slider

Thumbnail navigation below the main slider.

JS Thumbnails Gallery

Vertical Slider

Defilement vertical haut/bas.

CSS Vertical Y-axis

Parallax Slider

Parallax effect on slide backgrounds.

CSS Parallax Depth

Infinite Loop

Infinite scrolling with no visible end.

JS Infinite Touch

Basic Lightbox

Click on an image to open it in a modal with fade effect.

JS Modal Fade
View source code
HTML
<div class="demo-preview">
  <div class="gallery-grid" id="basicGallery">
    <div class="gallery-item gradient-1" data-index="0"></div>
    <div class="gallery-item gradient-2" data-index="1"></div>
    <div class="gallery-item gradient-3" data-index="2"></div>
    <div class="gallery-item gradient-4" data-index="3"></div>
  </div>
  <div class="lightbox-overlay" id="basicLightbox">
    <div class="lightbox-content">
      <button class="lightbox-close" aria-label="Close">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path d="M18 6L6 18M6 6l12 12"/>
        </svg>
      </button>
      <div class="lightbox-image gradient-1"></div>
    </div>
  </div>
</div>
CSS
.gallery-item  {
   width: 120px;
   height: 80px;
   border-radius: var(--radius-md);
   cursor: pointer;
   transition: transform 0.3s, box-shadow 0.3s;
   position: relative;
   overflow: hidden;
   }

.gallery-item:hover  {
   transform: scale(1.05);
   box-shadow: rgba(99, 102, 241, 0.3) 0px 10px 30px;
   }

.gradient-1  {
   background: linear-gradient(135deg, rgb(99, 102, 241), rgb(139, 92, 246));
   }

.gradient-2  {
   background: linear-gradient(135deg, rgb(217, 70, 239), rgb(244, 63, 94));
   }

.gradient-3  {
   background: linear-gradient(135deg, rgb(6, 182, 212), rgb(16, 185, 129));
   }

.gradient-4  {
   background: linear-gradient(135deg, rgb(245, 158, 11), rgb(239, 68, 68));
   }

.gallery-grid  {
   display: flex;
   flex-wrap: wrap;
   gap: 10px;
   justify-content: center;
   max-width: 280px;
   }

.masonry-grid .gallery-item  {
   width: 100%;
   break-inside: avoid;
   margin-bottom: 8px;
   }

.masonry-grid .gallery-item:nth-child(1)  {
   height: 60px;
   }

.masonry-grid .gallery-item:nth-child(2)  {
   height: 90px;
   }

.masonry-grid .gallery-item:nth-child(3)  {
   height: 70px;
   }

.masonry-grid .gallery-item:nth-child(4)  {
   height: 80px;
   }

.masonry-grid .gallery-item:nth-child(5)  {
   height: 50px;
   }

.masonry-grid .gallery-item:nth-child(6)  {
   height: 100px;
   }

/* Variables du design system du site, dont ce code depend. Sans elles,
   l'acheteur obtenait des coins carres et des couleurs par defaut (revue Owen).
   Valeurs reprises de css/style.css — a adapter librement. */
.gallery-grid {
  --radius-md: 0.5rem;
}

/* styles restaurés (1b523df9~1) */
.demo-preview {
  --radius-sm: 0.375rem;
  --radius-lg: 0.75rem;
  --radius-xl: 1rem;
  --text-muted: #8b8b93;
}

/* Lightbox Overlay Base */
.lightbox-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s ease, visibility 0.4s ease;
  backdrop-filter: blur(10px);
}
.lightbox-overlay.active {
  opacity: 1;
  visibility: visible;
}

/* Lightbox Content */
.lightbox-content {
  max-width: 80vw;
  max-height: 80vh;
  border-radius: var(--radius-xl);
  transform: scale(0.8);
  transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  position: relative;
}
.lightbox-overlay.active .lightbox-content {
  transform: scale(1);
}

/* Lightbox Image Placeholder */
.lightbox-image {
  width: 600px;
  height: 400px;
  max-width: 80vw;
  max-height: 70vh;
  border-radius: var(--radius-xl);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  font-weight: 600;
  color: white;
}

/* Close Button */
.lightbox-close {
  position: absolute;
  top: -40px;
  right: 0;
  width: 36px;
  height: 36px;
  background: rgba(255, 255, 255, 0.1);
  border: none;
  border-radius: 50%;
  color: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.3s ease, transform 0.3s ease;
}
.lightbox-close:hover {
  background: rgba(255, 255, 255, 0.2);
  transform: rotate(90deg);
}
JavaScript
(function() {
  // ==========================================
  // 1. Basic Lightbox
  // ==========================================
  (function() {
  const basicGallery = document.getElementById('basicGallery');
  const basicLightbox = document.getElementById('basicLightbox');
  if (!basicGallery || !basicLightbox) return;
  const basicImage = basicLightbox.querySelector('.lightbox-image');

  basicGallery.querySelectorAll('.gallery-item').forEach(item => {
    item.addEventListener('click', () => {
      const gradient = Array.from(item.classList).find(c => c.startsWith('gradient-'));
      basicImage.className = 'lightbox-image ' + gradient;
      basicLightbox.classList.add('active');
    });
  });

  basicLightbox.querySelector('.lightbox-close').addEventListener('click', () => {
    basicLightbox.classList.remove('active');
  });
  basicLightbox.addEventListener('click', (e) => {
    if (e.target === basicLightbox) basicLightbox.classList.remove('active');
  });
  })();

  // ==========================================
  // Global Keyboard Navigation
  // ==========================================
  document.addEventListener('keydown', (e) => {
    // Find which lightbox is active
    const activeLightbox = document.querySelector('.lightbox-overlay.active');
    if (!activeLightbox) return;

    switch (e.key) {
      case 'Escape':
        activeLightbox.classList.remove('active');
        if (document.exitFullscreen && document.fullscreenElement) {
          document.exitFullscreen();
        }
        break;
      case 'ArrowLeft':
        const prevBtn = activeLightbox.querySelector('.lightbox-nav.prev');
        if (prevBtn) prevBtn.click();
        break;
      case 'ArrowRight':
        const nextBtn = activeLightbox.querySelector('.lightbox-nav.next');
        if (nextBtn) nextBtn.click();
        break;
    }
  });
})();

Lightbox with Navigation

Previous/next arrows and keyboard navigation.

JS Navigation Keyboard

Zoom Lightbox

The image enlarges from its thumbnail position.

JS Zoom Transform

Lightbox with Thumbnails

Thumbnail strip at the bottom of the lightbox.

JS Thumbnails Strip

Fullscreen Lightbox

True fullscreen mode, Escape to close.

JS Fullscreen Immersive

Lightbox with Caption

Animated caption that appears below the image.

JS Caption Animated
Hover Me

Hover Zoom

The image zooms in on hover within its container.

CSS Transform
View source code
HTML
<div class="demo-preview">
  <div class="zoom-container hover-zoom">
    <div class="zoom-image grid-pattern">
      <span class="zoom-label">Hover Me</span>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Variables du design system du site, dont ce code dépend (valeurs de css/style.css — à adapter librement). */
.demo-preview {
  --bg-card: #12121a;
  --border: rgba(255, 255, 255, 0.08);
  --radius-lg: 0.75rem;
}

.zoom-container {
  width: 280px;
  height: 200px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  position: relative;
  cursor: pointer;
  background: var(--bg-card);
  border: 1px solid var(--border);
}

.zoom-image {
  width: 100%;
  height: 100%;
  background:
    repeating-linear-gradient(45deg,
      transparent,
      transparent 10px,
      rgba(99, 102, 241, 0.1) 10px,
      rgba(99, 102, 241, 0.1) 20px),
    linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
  background-size: 100% 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: 600;
  font-size: 1.2rem;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
  position: relative;
}

.zoom-image::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.3) 0%, transparent 50%),
    radial-gradient(circle at 70% 70%, rgba(0, 0, 0, 0.2) 0%, transparent 50%);
}

.grid-pattern {
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px),
    repeating-linear-gradient(45deg,
      transparent,
      transparent 10px,
      rgba(99, 102, 241, 0.1) 10px,
      rgba(99, 102, 241, 0.1) 20px),
    linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
  background-size: 20px 20px, 20px 20px, 100% 100%, 100% 100%;
}

.hover-zoom .zoom-image {
  transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.hover-zoom:hover .zoom-image {
  transform: scale(1.3);
}

.demo-preview:has(.zoom-container) {
  min-height: 240px;
}

.zoom-label {
  position: relative;
  z-index: 2;
}
JavaScript
/* Pure CSS — no JavaScript required */

Magnifying Glass

Circular magnifier that follows the cursor.

JS Background

Lens Zoom

Square magnifier that enlarges the area under the cursor.

JS Clip-path

Pan Zoom

Move mouse to explore the zoomed image.

JS Transform-origin

Click to Zoom

Click to alternate between normal and zoomed view.

JS Toggle

Zoom with Overlay

Zoomed view in a separate panel.

JS Overlay

Product Zoom

E-commerce style with thumbnails and hover.

JS E-commerce

Smooth Ken Burns

Slow zoom animation with continuous panning.

CSS Animation
Welcome to Effect.Labs Premium Web Effects Modern CSS Animations Infinite Scrolling
Welcome to Effect.Labs Premium Web Effects Modern CSS Animations Infinite Scrolling

Basic Marquee

Defilement horizontal simple et fluide.

CSS Minimal
View source code
HTML
<div class="demo-preview">
  <div class="marquee-container marquee-basic">
    <div class="marquee-track">
      <div class="marquee-content">
        <span>Welcome to Effect.Labs</span>
        <span>Premium Web Effects</span>
        <span>Modern CSS Animations</span>
        <span>Infinite Scrolling</span>
      </div>
      <div class="marquee-content">
        <span>Welcome to Effect.Labs</span>
        <span>Premium Web Effects</span>
        <span>Modern CSS Animations</span>
        <span>Infinite Scrolling</span>
      </div>
    </div>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.marquee-container {
  overflow: hidden;
  width: 100%;
  position: relative;
}

.marquee-track {
  display: flex;
  width: max-content;
  will-change: transform;
}

.marquee-content {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.marquee-basic .marquee-track {
  animation: marqueeScroll 15s linear infinite;
}

.marquee-basic .marquee-content {
  padding-right: 4rem;
  font-size: 1.5rem;
  font-weight: 600;
  color: #6366f1;
}

.marquee-basic .marquee-content span {
  margin: 0 2rem;
  white-space: nowrap;
}

.demo-preview:has(.marquee-container),
.demo-preview:has(.marquee-vertical),
.demo-preview:has(.marquee-alternating),
.demo-preview:has(.marquee-speed-container) {
  min-height: 180px;
  padding: 1rem 0;
}

.demo-preview .marquee-container,
.demo-preview .marquee-speed-container,
.demo-preview .marquee-alternating,
.demo-preview .marquee-vertical {
  width: 100%;
}

@keyframes marqueeScroll {
  0% {
    transform: translateX(0);
  }

  100% {
    transform: translateX(-50%);
  }
}
JavaScript
/* Pure CSS — no JavaScript required */

Reverse Marquee

Text scrolling in the opposite direction.

CSS Reverse

Vertical Marquee

Vertical scrolling for announcements.

CSS Vertical

Image Marquee

Images defilantes horizontalement.

CSS Images

Speed Variation

Differentes vitesses de defilement.

CSS Speed

Hover Pause

Pauses on mouse hover.

CSS Interactive

Gradient Fade

Gradient fade on edges.

CSS Gradient

Alternating Rows

Rows alternating directions.

CSS Multi-row

Logo Ticker

Bandeau de logos de partenaires.

CSS Logos

Marquee with Icons

Text mixed with icons and symbols.

CSS Icons

WebGL Displacement Slideshow

Slideshow whose transitions are computed by a GPU displacement shader: visuals warp and blend into each other. CSS crossfade fallback if WebGL is unavailable.

💡 Perfect for: visual hero, premium product gallery, agency showreel, photo portfolio, immersive homepage.

WebGL Shader Slideshow Premium
SEAMLESS INFINITE VELOCITY MARQUEE NO GAP SCROLL PHYSICS
NEW DROP EFFECT.LABS VANILLA JS NO FRAMEWORK OPEN SOURCE CSS + JS
INERTIA SKEWX MASK-IMAGE RAF + LERP A11Y WCAG 2.2.2

Velocity Marquee

Seamless infinite band: content is duplicated as needed, position accumulated in JS (rAF) and folded modulo one set width — no jump, no gap. Page scroll and horizontal mouse movement accelerate the band with inertia and tilt the text (clamped skewX). Accessible pause button; under prefers-reduced-motion: slow constant scroll, no tilt.

💡 Perfect for: partner logo strips, « Trusted by » sections, press tickers, agency heroes.

Scroll Physics Seamless Loop Pointer Velocity rAF + lerp skewX
View source code
HTML
<div class="demo-preview">
  <div class="vmq-demo-wrapper">
    <span class="vmq-demo-label" aria-hidden="true">move the cursor · scroll the page</span>

    <!-- bande secondaire haute (RTL, petite) -->
    <div class="vmq-stripe vmq-stripe--sm">
      <div class="vmq-root" data-dir="rtl"
           aria-label="Seamless · Infinite · Velocity · Marquee"
           style="--vmq-size:clamp(13px,2.4vw,20px);--vmq-gap:18px;--vmq-sep-gap:10px;">
        <div class="vmq-track">
          <span class="vmq-item" data-style="outline">SEAMLESS</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">INFINITE</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">VELOCITY</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">MARQUEE</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">NO GAP</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">SCROLL PHYSICS</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
        </div>
      </div>
    </div>

    <!-- bande principale (grande typo, LTR) -->
    <div class="vmq-stripe vmq-stripe--lg">
      <div class="vmq-root" aria-label="Marquee Velocity Effect : NEW DROP · EFFECT.LABS · VANILLA JS · NO FRAMEWORK · OPEN SOURCE"
           style="--vmq-size:clamp(30px,6vw,56px);--vmq-gap:24px;">
        <div class="vmq-track">
          <span class="vmq-item">NEW DROP</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
          <span class="vmq-item" data-style="outline">EFFECT.LABS</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
          <span class="vmq-item">VANILLA JS</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
          <span class="vmq-item" data-style="outline">NO FRAMEWORK</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
          <span class="vmq-item">OPEN SOURCE</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
          <span class="vmq-item" data-style="outline">CSS + JS</span>
          <span class="vmq-sep" aria-hidden="true">✦</span>
        </div>
      </div>
    </div>

    <!-- bande secondaire basse (RTL, petite) -->
    <div class="vmq-stripe vmq-stripe--sm">
      <div class="vmq-root" data-dir="rtl"
           aria-label="Inertia · skewX · mask-image · rAF · lerp"
           style="--vmq-size:clamp(13px,2.4vw,20px);--vmq-gap:18px;--vmq-sep-gap:10px;">
        <div class="vmq-track">
          <span class="vmq-item" data-style="outline">INERTIA</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">SKEWX</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">MASK-IMAGE</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">RAF + LERP</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">A11Y</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
          <span class="vmq-item" data-style="outline">WCAG 2.2.2</span>
          <span class="vmq-sep" aria-hidden="true">·</span>
        </div>
      </div>
    </div>

    <button class="vmq-pause-btn" id="vmqPauseBtn"
            aria-label="Mettre en pause le défilement"
            aria-pressed="false"
            type="button">
      <span aria-hidden="true">⏸</span>
      <span>Pause</span>
    </button>
  </div>
</div>
CSS
.demo-preview,
.demo-preview *,
.demo-preview *::before,
.demo-preview *::after {
  box-sizing: border-box
}

.demo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.vmq-root {
  position: relative;
  overflow: hidden;
  width: 100%;
  cursor: default;
  -webkit-mask-image: linear-gradient(to right,
      transparent 0%,
      #000 9%,
      #000 91%,
      transparent 100%);
  mask-image: linear-gradient(to right,
      transparent 0%,
      #000 9%,
      #000 91%,
      transparent 100%);
}

@supports not ((mask-image: linear-gradient(to right, #000, #000)) or (-webkit-mask-image: linear-gradient(to right, #000, #000))) {
  .vmq-root {
    border-radius: 0;
  }
}

.vmq-track {
  display: flex;
  align-items: center;
  white-space: nowrap;
  will-change: transform;
  transform-origin: left center;
  user-select: none;
  -webkit-user-select: none;
}

.vmq-item {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  padding: 0 var(--vmq-gap, 28px);
  font-size: var(--vmq-size, clamp(24px, 5vw, 48px));
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--vmq-color, #f1f5f9);
  white-space: nowrap;
  pointer-events: none;
}

.vmq-item[data-style="outline"] {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--vmq-stroke, rgba(241, 245, 249, 0.45));
  text-stroke: 1.5px var(--vmq-stroke, rgba(241, 245, 249, 0.45));
}

.vmq-sep {
  flex-shrink: 0;
  padding: 0 var(--vmq-sep-gap, 16px);
  font-size: calc(var(--vmq-size, clamp(24px, 5vw, 48px)) * 0.60);
  color: var(--vmq-accent, #6366f1);
  line-height: 1;
  pointer-events: none;
  white-space: nowrap;
}

.vmq-demo-wrapper {
  position: relative;
  width: 100%;
  min-height: 300px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  background: #06060e;
  border-radius: 14px;
  overflow: hidden;
  gap: 0;
}

.vmq-demo-label {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 11px;
  color: rgba(148, 163, 184, 0.30);
  letter-spacing: 0.10em;
  text-transform: uppercase;
  font-weight: 500;
  pointer-events: none;
  white-space: nowrap;
  z-index: 2;
}

.vmq-stripe {
  display: flex;
  align-items: center;
  flex: 1;
  min-height: 60px;
}

.vmq-stripe--lg {
  flex: 2;
  min-height: 100px;
}

.vmq-stripe--sm {
  flex: 1;
  opacity: 0.6;
  min-height: 56px;
}

.vmq-pause-btn {
  position: absolute;
  bottom: 14px;
  right: 14px;
  z-index: 10;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(15, 15, 26, 0.80);
  border: 1px solid rgba(99, 102, 241, 0.35);
  color: #c7d2fe;
  padding: 6px 12px 6px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  letter-spacing: 0.01em;
  transition: background 0.15s, border-color 0.15s;
  outline: 2px solid transparent;
  outline-offset: 2px;
}

.vmq-pause-btn:hover {
  background: rgba(30, 30, 50, 0.90);
  border-color: rgba(99, 102, 241, 0.65);
}

.vmq-pause-btn:focus-visible {
  outline-color: #818cf8;
}

.vmq-pause-btn[aria-pressed="true"] {
  background: rgba(49, 46, 129, 0.85);
  border-color: #6366f1;
  color: #e0e7ff;
}

@media (prefers-reduced-motion: reduce) {
  .vmq-track {

    transition: none !important;
  }

  .vmq-pause-btn {
    transition: none;
  }
}
JavaScript
(function() {
  /* fx-0831 */
  (function() {
    'use strict';

    /* --- constantes --- */
    var BASE_SPEED = 0.85;
    var SCROLL_K = 0.06;
    var MOUSE_K = 0.028;
    var BREATHE_K = 0.10;
    var LERP_VEL = 0.07;
    var LERP_SKEW = 0.09;
    var SKEW_K = 0.28;
    var SKEW_MAX = 7;

    /* --- état global --- */
    var vel = BASE_SPEED;
    var skew = 0;
    var tSkew = 0;
    var scrollBonus = 0;
    var mouseBonus = 0;
    var hoverCount = 0;
    var paused = false;
    var rafId = null;
    var time = 0;
    var lastScrollY = 0;
    var lastMouseX = null;
    var reduced = false;
    var roots = [];
    var resizeTimer = null;

    /* --- initialisation --- */
    function init() {
      lastScrollY = window.scrollY || 0;
      var mq = window.matchMedia('(prefers-reduced-motion: reduce)');
      reduced = mq.matches;
      mq.addEventListener('change', function(e) {
        reduced = e.matches;
        if (reduced) {
          skew = 0;
          tSkew = 0;
        }
      });

      roots = Array.prototype.slice.call(document.querySelectorAll('.vmq-root'));
      roots.forEach(setup);

      window.addEventListener('scroll', onScroll, {
        passive: true
      });
      window.addEventListener('mousemove', onMouse, {
        passive: true
      });
      window.addEventListener('resize', onResize);

      rafId = requestAnimationFrame(tick);
    }

    /* --- setup d'une racine --- */
    function setup(root) {
      var track = root.querySelector('.vmq-track');
      if (!track) return;

      var origItems = Array.prototype.slice.call(track.children);
      var origCount = origItems.length;

      function dup() {
        while (track.children.length > origCount) {
          track.removeChild(track.lastChild);
        }
        var cw = root.getBoundingClientRect().width || 480;
        var sw = track.scrollWidth || 200;
        var copies = Math.ceil(2 * cw / sw) + 2;
        for (var i = 0; i < copies; i++) {
          origItems.forEach(function(item) {
            track.appendChild(item.cloneNode(true));
          });
        }
        root._vmq = root._vmq || {};
        root._vmq.track = track;
        root._vmq.setW = sw;
        root._vmq.pos = root._vmq.pos || 0;
        root._vmq.rtl = root.dataset.dir === 'rtl';
        root._vmq.dup = dup;
      }
      dup();

      root.addEventListener('mouseenter', function() {
        hoverCount++;
      });
      root.addEventListener('mouseleave', function() {
        hoverCount = Math.max(0, hoverCount - 1);
      });
      root.addEventListener('focusin', function() {
        hoverCount++;
      });
      root.addEventListener('focusout', function() {
        hoverCount = Math.max(0, hoverCount - 1);
      });

      if (window.IntersectionObserver) {
        new IntersectionObserver(function(entries) {
          root._vmq.visible = entries[0].isIntersecting;
        }, {
          threshold: 0
        }).observe(root);
      }
      root._vmq.visible = true;
    }

    /* --- scroll / mouse --- */
    function onScroll() {
      var dy = (window.scrollY || 0) - lastScrollY;
      lastScrollY = window.scrollY || 0;
      scrollBonus = dy * SCROLL_K;
    }

    function onMouse(e) {
      if (lastMouseX !== null) {
        mouseBonus = (e.clientX - lastMouseX) * MOUSE_K;
      }
      lastMouseX = e.clientX;
    }

    function onResize() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function() {
        roots.forEach(function(root) {
          if (root._vmq && root._vmq.dup) root._vmq.dup();
        });
      }, 140);
    }

    /* --- boucle principale --- */
    function tick() {
      rafId = requestAnimationFrame(tick);
      if (paused) return;

      time += 0.009;
      var breathe = reduced ? 0 : Math.sin(time) * BREATHE_K;

      var tVel;
      if (reduced) {
        tVel = BASE_SPEED * 0.22;
      } else if (hoverCount > 0) {
        tVel = BASE_SPEED * 0.25;
      } else {
        tVel = BASE_SPEED + scrollBonus + mouseBonus + breathe;
      }

      vel += (tVel - vel) * LERP_VEL;
      scrollBonus *= 0.80;
      mouseBonus *= 0.84;

      if (reduced || hoverCount > 0) {
        tSkew = 0;
      } else {
        tSkew = -(vel - BASE_SPEED) * SKEW_K;
        if (tSkew > SKEW_MAX) tSkew = SKEW_MAX;
        if (tSkew < -SKEW_MAX) tSkew = -SKEW_MAX;
      }
      skew += (tSkew - skew) * LERP_SKEW;

      roots.forEach(function(root) {
        if (!root._vmq || !root._vmq.visible) return;
        var sw = root._vmq.setW;
        if (!sw || sw < 1) return;
        var step = root._vmq.rtl ? -vel : vel;
        root._vmq.pos = ((root._vmq.pos + step) % sw + sw) % sw;
        var sk = root._vmq.rtl ? -skew : skew;
        root._vmq.track.style.transform =
          'translateX(-' + root._vmq.pos.toFixed(2) + 'px) skewX(' + sk.toFixed(3) + 'deg)';
      });
    }

    /* --- API publique --- */
    window.VelocityMarquee = {
      pause: function() {
        paused = true;
      },
      play: function() {
        paused = false;
      },
      toggle: function() {
        paused = !paused;
        return paused;
      },
      setSpeed: function(v) {
        BASE_SPEED = Math.max(0.1, Math.min(5, Number(v) || BASE_SPEED));
      },
      setSkewMax: function(v) {
        SKEW_MAX = Math.max(0, Math.min(20, Number(v) || SKEW_MAX));
      }
    };

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      init();
    }

  })();
})();

Cylinder Marquee 3D

A pure CSS 3D cylinder: N faces positioned with rotateY + translateZ, radius auto-computed. Continuous rAF rotation, inertial pointer drag, cosine attenuation on back faces, pause button. Flat scrolling row if the browser lacks transform-style: preserve-3d; reduced motion stops the spin and exposes ‹ › buttons.

💡 Perfect for: testimonial halls of fame, tech-stack showcases, client/partner carousels, portfolio skills sections.

CSS 3D preserve-3d Drag inertiel rAF + lerp Pointer Capture Premium

Text-Path Marquee

Seamless text marquee across three sinusoidal SVG paths — no canvas, no framework. The wave pulses in amplitude and drifts in phase; the cursor raises a local Gaussian bump that deforms the path in real time (lerp). Solid/outline track alternation for depth, pause on hover/focus plus an accessible button. Under prefers-reduced-motion: frozen paths, static text.

💡 Perfect for: animated landing-page heroes, wavy section headers, product-launch tickers, festival or concert stage badges.

SVG textPath Wave Path rAF + lerp Pointer Deform WCAG 2.2.2 Premium

Live Ticker

Continuously scrolling ticker with pure DOM recycling: no node duplication, no visible jump. The LiveTicker.push() API injects or updates items on the fly with a green/red flash; inertial drag, pause on hover/focus and a pause button make it fully interactive. Under prefers-reduced-motion: a static, horizontally scrollable list.

💡 Perfect for: stock quote bars, SaaS announcement strips (changelog, system alerts, product updates), live notifications, breaking-news tickers.

DOM Recycling rAF + Inertie Pointer Events Live API push Flash delta Premium

Lens Marquee

Two text layers scroll in perfect sync: an always-visible monochrome outline layer and a gradient-filled layer revealed only through a glowing circular lens that follows the pointer — or sweeps on its own. Pause on hover/focus plus an accessible button; the fill stays visible if mask-image is unsupported, and the band freezes under prefers-reduced-motion.

💡 Perfect for: creative agency heroes, tech-stack showcases, premium product launches, brand identity pages.

CSS Mask Dual Layer rAF + lerp Pointer Tracking Auto-oscillation WCAG 2.2.2 Premium

Frequently Asked Questions

How to create an infinite carousel in CSS? +

Duplicate the item list. Apply animation: scroll Xs linear infinite. When the animation reaches the end of the first list, it seamlessly restarts because the second list is identical. Use overflow:hidden on the container.

How to make a responsive slider without a library? +

Use CSS scroll-snap: overflow-x:auto, scroll-snap-type:x mandatory on the container, scroll-snap-align:start on each slide. Arrow buttons use scrollBy() in JavaScript. Zero dependencies.

What is the best carousel for images? +

For full-screen images, use a slider with transform transition. For thumbnails, a carousel with scroll-snap + thumbnail navigation. For a portfolio, a masonry grid with lightbox on click.

How to optimize carousel performance? +

Load images with lazy loading (loading='lazy'). Pre-load only the next slide. Use images in the right format (WebP) and at the right size. For marquees, use will-change:transform.

How to make a carousel accessible? +

Add role='region' and aria-label on the container. Arrow buttons have aria-label='Previous'/'Next'. Each slide has role='group' and aria-roledescription='slide'. Support keyboard navigation with arrow keys.