Gallery & Media
Carousel, lightbox, zoom, before/after comparison and marquee.
Updated
Do you need a JavaScript library for a carousel or an image gallery?
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.
View source code
<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>.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;
}(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.
Auto-play Slider
Auto-scrolling with progress bar.
Thumbnail Slider
Thumbnail navigation below the main slider.
Card Carousel
Multiple visible cards, horizontal scrolling.
3D Carousel
3D perspective carousel: click to rotate through the faces.
Vertical Slider
Defilement vertical haut/bas.
Parallax Slider
Parallax effect on slide backgrounds.
Infinite Loop
Infinite scrolling with no visible end.
Basic Lightbox
Click on an image to open it in a modal with fade effect.
View source code
<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>.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);
}(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;
}
});
})();Zoom Lightbox
The image enlarges from its thumbnail position.
Gallery Grid
Masonry-style grid that opens a lightbox.
Lightbox with Thumbnails
Thumbnail strip at the bottom of the lightbox.
Fullscreen Lightbox
True fullscreen mode, Escape to close.
Lightbox with Caption
Animated caption that appears below the image.
Swipe Gallery
Touch gallery with swipe gesture.
Hover Zoom
The image zooms in on hover within its container.
View source code
<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>.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;
}/* Pure CSS — no JavaScript required */Magnifying Glass
Circular magnifier that follows the cursor.
Lens Zoom
Square magnifier that enlarges the area under the cursor.
Pan Zoom
Move mouse to explore the zoomed image.
Click to Zoom
Click to alternate between normal and zoomed view.
Zoom with Overlay
Zoomed view in a separate panel.
Product Zoom
E-commerce style with thumbnails and hover.
Smooth Ken Burns
Slow zoom animation with continuous panning.
Basic Marquee
Defilement horizontal simple et fluide.
View source code
<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>.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%);
}
}/* Pure CSS — no JavaScript required */Reverse Marquee
Text scrolling in the opposite direction.
Vertical Marquee
Vertical scrolling for announcements.
Image Marquee
Images defilantes horizontalement.
Speed Variation
Differentes vitesses de defilement.
Hover Pause
Pauses on mouse hover.
Gradient Fade
Gradient fade on edges.
Alternating Rows
Rows alternating directions.
Logo Ticker
Bandeau de logos de partenaires.
Marquee with Icons
Text mixed with icons and symbols.
CSS-Only Carousel
Native carousel without a single line of JavaScript: arrows and dots generated by the browser (::scroll-button / ::scroll-marker), smooth scroll-snap. Lightweight, accessible, robust.
💡 Perfect for: product gallery, testimonials, partner logos, onboarding, hero slider — anywhere you used to write 30 lines of JS.
View source code
<div class="demo-preview">
<div class="coc-wrap">
<div class="coc-carousel" tabindex="0" aria-label="Carousel">
<div class="coc-slide" style="--g:linear-gradient(135deg,#6d8bff,#5cffd8)">01</div>
<div class="coc-slide" style="--g:linear-gradient(135deg,#ff7eb6,#a777ff)">02</div>
<div class="coc-slide" style="--g:linear-gradient(135deg,#ffb86b,#ff5e8a)">03</div>
<div class="coc-slide" style="--g:linear-gradient(135deg,#4dd2ff,#9b8cff)">04</div>
<div class="coc-slide" style="--g:linear-gradient(135deg,#5cffd8,#3ad6b3)">05</div>
</div>
</div>
</div>.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;
}
.coc-wrap {
position: relative;
}
.coc-carousel {
display: flex;
gap: 14px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
padding: 6px;
border-radius: 14px;
scrollbar-width: none;
scroll-marker-group: after;
}
.coc-carousel::-webkit-scrollbar {
height: 0;
}
.coc-slide {
flex: 0 0 78%;
scroll-snap-align: center;
aspect-ratio: 16 / 9;
border-radius: 14px;
display: grid;
place-items: center;
color: #fff;
font-weight: 800;
font-size: 20px;
letter-spacing: -0.01em;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
background: var(--g, linear-gradient(135deg, #6d8bff, #5cffd8));
}
.coc-slide::scroll-marker {
content: "";
width: 9px;
height: 9px;
margin: 0 4px;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.28);
cursor: pointer;
transition: background .2s, transform .2s;
}
.coc-slide::scroll-marker:target-current {
background: #5cffd8;
transform: scale(1.3);
}
.coc-slide::scroll-marker:hover {
background: rgba(255, 255, 255, 0.6);
}
.coc-carousel::scroll-marker-group {
display: flex;
justify-content: center;
align-items: center;
padding-top: 12px;
}
.coc-carousel::scroll-button(left),
.coc-carousel::scroll-button(right) {
position: absolute;
top: calc(50% - 28px);
transform: translateY(-50%);
z-index: 3;
width: 38px;
height: 38px;
border: 0;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
background: rgba(10, 14, 31, 0.65);
color: #fff;
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
}
.coc-carousel::scroll-button(left) {
left: 10px;
content: "‹";
}
.coc-carousel::scroll-button(right) {
right: 10px;
content: "›";
}
.coc-carousel::scroll-button(left):disabled,
.coc-carousel::scroll-button(right):disabled {
opacity: .35;
cursor: default;
}
@media (prefers-reduced-motion: reduce) {
.coc-carousel {
scroll-behavior: auto;
}
}/* Pure CSS — no JavaScript required */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.
Gallery Flow Morph
Scattered gallery drifting in parallax: click a tile and it morphs fullscreen through the View Transitions API, title revealed letter by letter. Fade fallback, keyboard dismiss.
💡 Perfect for: portfolios, photo galleries, agency project pages, interactive moodboards.
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.
View source code
<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>.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;
}
}(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.
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.
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.
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.