Advanced Demos
These demos are custom UI patterns built on top of Snap — not built-in presets.
Most map coord, progress, and slide metadata in the update callback to CSS transforms. Some also use other vevet modules (Timeline, Swipe, Responsive, and others). Expand the HTML/CSS/JS snippets below each demo to see the full implementation.
Focus
(todo read code and describe)
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1015/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1018/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1021/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1025/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1035/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
<div class="slide">
<div class="slide_frame">
<img src="https://picsum.photos/id/1041/400/600" alt="" data-snap-parallax-x="10%" data-snap-parallax-y="4%">
</div>
</div>
</div>
</div>
CSS
.fullpage-center {
padding-inline: 1.25rem;
}
.carousel {
--container-height: min(80vw, 80svh);
--container-width: min(var(--container-height) * 1.5, 80vw);
position: relative;
width: var(--container-width);
height: var(--container-height);
overflow: hidden;
border-radius: calc(var(--container-height) * 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.1);
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
--slide-height: calc(var(--container-height) * 0.76);
--slide-width: calc(var(--slide-height) * 0.7);
position: absolute;
top: 12%;
left: 0;
width: var(--slide-width);
height: var(--slide-height);
transform-origin: center bottom;
will-change: transform, filter, opacity;
}
.slide_frame {
position: relative;
width: 100%;
height: 100%;
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 28px 70px rgba(0, 0, 0, 0.5);
}
.slide_frame img {
position: absolute;
top: -22%;
left: -22%;
width: 144%;
height: 144%;
object-fit: cover;
will-change: transform;
}
JavaScript
import {
Snap,
EaseOutBack
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
grabCursor: true,
origin: "center",
freemode: "sticky",
loop: true,
wheel: true,
wheelAxis: "y",
gap: "-1rem",
lerp: 0.11,
duration: 650,
easing: EaseOutBack
});
container.classList.add("ready");
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord,
progress
}) => {
const abs = Math.abs(progress);
const focus = Math.exp(-abs * abs * 1.15);
const scale = 0.7 + focus * 0.3;
const y = Math.sin(progress * 1.2) * (24 + abs * 10);
const rotate = progress * -14;
const blur = Math.max(0, (abs - 0.1) * 4.5);
const bright = 0.5 + focus * 0.5;
const opacity = 0.3 + focus * 0.7;
element.style.zIndex = String(Math.round(focus * 200));
element.style.opacity = opacity;
element.style.filter = `blur(${blur}px) brightness(${bright})`;
element.style.transform = `translate3d(${coord}px, ${y}px, 0) scale(${scale}) rotate(${rotate}deg)`;
});
});
Expand slide
Freemode carousel where clicking a slide expands its width with Timeline and calls slide.resize() + carousel.set().
Key props: freemode: "sticky", wheel: true, edgeFriction: 0.95, stickOnResize: false.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/1040/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1050/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1060/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1070/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1080/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1081/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
<div class="slide">
<img src="https://picsum.photos/id/1082/1000/1500" alt="" data-snap-parallax-scale="0.2" data-snap-parallax-scale-abs data-snap-parallax-scale-scope="none">
<p class="text">Click me</p>
</div>
</div>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.carousel {
position: relative;
width: 100%;
height: calc(85svh - 1rem);
opacity: 0;
transition: opacity 0.35s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: 45vw;
height: 100%;
border-radius: 1rem;
overflow: hidden;
}
.slide img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.slide .text {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1.5rem;
font-weight: bold;
white-space: nowrap;
}
JavaScript
import {
clamp,
lerp,
Snap,
Timeline,
vevet
} from "vevet";
const container = document.getElementById("carousel");
let tmIndex = 0;
const carousel = new Snap({
container: container,
direction: "horizontal",
gap: "1rem",
wheel: true,
wheelAxis: "y",
freemode: "sticky",
shortSwipes: false,
stickOnResize: false,
edgeFriction: 0.95,
grabCursor: true
},
{
onResize: () => {
container.classList.add("ready");
},
onUpdate: (data, {
slides
}) => {
slides.forEach(({
element,
coord
}) => {
element.style.transform = `translateX(${coord}px)`;
});
}
});
const toggleSlide = index => {
if (!carousel) {
return;
}
tmIndex = index;
const slide = carousel.slides[index];
const element = slide.element;
element.classList.toggle("active");
const isExpanding = element.classList.contains("active");
const fromWidth = element.offsetWidth / vevet.width * 100;
const startTrack = carousel.current;
const tm = new Timeline({
duration: 500
}, {
onUpdate: ({
eased
}) => {
const toWidth = isExpanding ? 80 : 45;
element.style.width = `${lerp(fromWidth, toWidth, eased)}vw`;
slide.resize();
if (tmIndex === index) {
if (isExpanding) {
carousel.set(
lerp(
startTrack,
clamp(slide.staticCoord, carousel.min, carousel.max),
eased));
} else {
carousel.clampTarget();
}
}
}
});
tm.play();
};
carousel.slides.forEach(({
element,
index
}) => {
element.addEventListener("click", () => toggleSlide(index));
});
Panorama
3D panorama carousel inspired by Panorama Slider.
Maps progress to translateZ, rotateY, and a horizontal offset in onUpdate. Uses origin: "center", freemode: true, and loop: true.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/758/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/760/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/770/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/790/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/800/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/810/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/820/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/830/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/840/400/600" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(40vw, 40svh);
position: relative;
width: 100%;
height: var(--size);
perspective: calc(var(--size) * 5.55);
transform-style: preserve-3d;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: calc(var(--size) * 1);
height: var(--size);
border-radius: 1rem;
overflow: hidden;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
vevet
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
grabCursor: true,
origin: "center",
freemode: true,
loop: true,
gap: 10,
duration: 300
});
carousel.on("update", () => {
const depth = 200;
const rotation = 20;
const scale = 1 / (180 / rotation);
const halfAngle = rotation * Math.PI / 180 / 2;
carousel.slides.forEach(({
element,
coord,
progress,
size
}) => {
const factor = 1 - Math.cos(progress * scale * Math.PI);
const xOffset = progress * (size / 3) * factor;
const zOffset = size * 0.5 / Math.sin(halfAngle) * factor - depth;
const rotateY = progress * rotation;
element.style.transform = `translateX(${
coord + xOffset
}px) translateZ(${zOffset}px) rotateY(${rotateY}deg)`;
});
});
container.classList.add("ready");
Cube
3D cube carousel with perspective (todo read code and describe)
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/10/700/700" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/28/700/700" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/37/700/700" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/161/700/700" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/180/700/700" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/649/700/700" alt="">
</div>
</div>
</div>
CSS
.fullpage-center {
padding-inline: 1.25rem;
}
.carousel {
--size: min(50vw, 50svh);
position: relative;
width: var(--size);
height: var(--size);
transform-style: preserve-3d;
perspective: 1000px;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
wheel: true,
wheelAxis: "y",
origin: "center",
loop: true,
freemode: "sticky",
interval: 1000,
duration: 750,
shortSwipes: false,
grabCursor: true,
onUpdate: (data, {
slides
}) => {
slides.forEach(({
element,
size,
progress
}) => {
let z = size * 0.5;
let rotate = progress * -90;
let x = 0;
let alpha = 1;
if (progress > 1 || progress < -1) {
z = 0;
rotate = 0;
x = 0;
alpha = 0;
}
element.style.transform = `rotateY(${rotate}deg) translate3d(${x}px, 0, ${z}px)`;
element.style.opacity = `${alpha}`;
});
}
});
container.classList.add("ready");
Circular carousel
Slides positioned on a circle with swipeAxis: "angle". Each slide's progress drives sin / cos placement and rotation.
HTML
<div class="fullpage-center">
<div class="container" id="container">
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/758/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/760/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/770/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/790/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/800/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/810/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/820/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/830/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-scale="-5" data-snap-parallax-scale-min="0.755" data-snap-parallax-scale-impulse data-snap-parallax-scale-abs data-snap-parallax-scale-scope="1,1" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-30" data-snap-parallax-skew-max="30" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/840/400/600" alt="">
</div>
</div>
</div>
</div>
CSS
.container {
--size: min(70svh, 70vw);
position: relative;
width: var(--size);
height: var(--size);
}
.slide {
--slide-size: calc(var(--size) / 5);
position: absolute;
top: calc(50% - var(--slide-size) / 2);
left: calc(50% - var(--slide-size) / 2);
width: var(--slide-size);
height: var(--slide-size);
display: flex;
justify-content: center;
align-items: center;
}
.wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
}
.wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap
} from "vevet";
const carousel = new Snap({
container: document.getElementById("container"),
direction: "horizontal",
wheel: true,
swipeAxis: "angle",
wheelAxis: "y",
loop: true,
freemode: true,
grabCursor: true
},
{
onUpdate: (data, {
containerSize,
slides
}) => {
const radius = containerSize / 2;
const p2 = Math.PI * 2;
const offset = Math.PI * -0.5;
slides.forEach(slide => {
const element = slide.element;
const progress = -slide.progress / slides.length;
const x = Math.cos(p2 * progress + offset) * radius;
const y = Math.sin(p2 * progress + offset) * radius;
const rotation = p2 * progress;
element.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rotation}rad)`;
});
}
});
Cylinder carousel
Horizontal cylinder layout: slides orbit with translateX / translateZ from circular math in onUpdate.
HTML
<div class="fullpage-center">
<div class="container" id="container">
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/758/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/760/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/770/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/790/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/800/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/780/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/810/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/820/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/830/400/600" alt="">
</div>
</div>
<div class="slide">
<div class="wrapper" data-snap-parallax-skew="-200" data-snap-parallax-skew-min="-50" data-snap-parallax-skew-max="50" data-snap-parallax-skew-impulse data-snap-parallax-skew-directional data-snap-parallax-skew-scope="1,1">
<img src="https://picsum.photos/id/840/400/600" alt="">
</div>
</div>
</div>
</div>
CSS
.container {
--size: min(70svh, 70vw);
position: relative;
width: var(--size);
height: var(--size);
transform-style: preserve-3d;
perspective: calc(var(--size) * 1.5);
}
.slide {
--slide-size: calc(var(--size) / 4);
position: absolute;
top: calc(50% - var(--slide-size) / 2);
left: calc(50% - var(--slide-size) / 2);
width: var(--slide-size);
height: var(--slide-size);
display: flex;
justify-content: center;
align-items: center;
}
.wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
}
.wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap
} from "vevet";
const carousel = new Snap({
container: document.getElementById("container"),
direction: "horizontal",
wheel: true,
wheelAxis: "y",
loop: true,
freemode: true,
grabCursor: true
},
{
onUpdate: (data, {
containerSize,
slides
}) => {
const radius = containerSize / 2;
const p2 = Math.PI * 2;
const offset = Math.PI * -0.5;
slides.forEach(slide => {
const element = slide.element;
const progress = -slide.progress / slides.length;
const x = Math.cos(p2 * progress + offset) * radius;
const z = -Math.sin(p2 * progress + offset) * radius;
const rotation = p2 * progress;
element.style.transform = `translateX(${x}px) translateZ(${z}px) rotateY(${rotation}rad)`;
});
}
});
Perspective carousel
CSS 3D perspective carousel. Maps progress to translateZ and rotateZ per slide.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/757/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/59/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/400/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/260/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/358/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/478/400/600" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/625/400/600" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(70vw, 70svh);
position: relative;
width: 100%;
height: var(--size);
perspective: calc(var(--size) * 1.6);
transform-style: preserve-3d;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: calc(var(--size) * 0.6);
height: var(--size);
border-radius: 1rem;
overflow: hidden;
box-shadow: 0px 0px 50px 0px rgba(0, 0, 0, 0.1);
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
vevet
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
grabCursor: true,
wheel: true,
wheelAxis: "y",
origin: "center",
loop: true,
shortSwipes: false,
freemode: "sticky"
});
carousel.on("update", () => {
carousel.slides.forEach(({
element,
size,
coord,
progress
}) => {
const z = Math.abs(progress ** 2) * -(size * 0.625);
const rZ = progress * 5;
element.style.transform = `translateX(${coord}px) translateZ(${z}px) rotateZ(${rZ}deg)`;
});
});
container.classList.add("ready");
Wavy sine
Horizontal track with sine-wave vertical offset, rotation, and scale per slide.
Uses eventsEmitter: document.documentElement, freemode: true, and origin: "center".
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide"><img src="https://picsum.photos/id/12/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/47/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/83/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/119/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/156/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/198/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/234/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/276/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/301/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/349/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/377/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/412/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/445/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/488/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/521/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/556/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/602/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/643/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/689/600/900" alt=""></div>
<div class="slide"><img src="https://picsum.photos/id/732/600/900" alt=""></div>
</div>
</div>
CSS
.carousel {
--size: min(30vw, 30vh);
position: relative;
width: calc(var(--size) * 0.625);
height: var(--size);
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0px 0px 50px 0px rgba(0, 0, 0, 0.1);
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
clamp
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
eventsEmitter: document.documentElement,
direction: "horizontal",
origin: "center",
loop: true,
gap: 30,
grabCursor: true,
freemode: true
});
carousel.on("update", (data, {
slides
}) => {
const sineFrequency = 0.25;
const sineAmp = 0.5;
const rotationAmp = -30;
const scaleAmp = 0.2;
slides.forEach(({
element,
size,
progress,
coord
}) => {
const x = coord;
const y = Math.sin(progress * Math.PI * sineFrequency) * size * sineAmp;
const rotate = Math.cos(progress * Math.PI * sineFrequency) * rotationAmp;
const scale = clamp(1 - Math.abs(progress) * scaleAmp);
element.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rotate}deg) scale(${scale})`;
});
});
container.classList.add("ready");
Expo slider
Expo-style carousel inspired by Expo Slider.
Smooth motion via easing: EaseOutExpo, duration: 1500, lerp: 0.2.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<div class="wrap" data-snap-parallax-scale="0.25" data-snap-parallax-scale-abs data-snap-parallax-x="-12.5%" data-snap-parallax-x-scope="-2,2" data-snap-parallax-rotate-y="10deg">
<img class="image" src="https://picsum.photos/id/758/1920/1080" data-snap-parallax-x="20%" data-snap-parallax-opacity="-0.2" alt="">
<p class="title" data-snap-parallax-x="20vw" data-snap-parallax-opacity="-2" data-snap-parallax-opacity-scope="-0.5,0.5">Title 1</p>
</div>
</div>
<div class="slide">
<div class="wrap" data-snap-parallax-scale="0.25" data-snap-parallax-scale-abs data-snap-parallax-x="-12.5%" data-snap-parallax-x-scope="-2,2" data-snap-parallax-rotate-y="10deg">
<img class="image" src="https://picsum.photos/id/760/1920/1080" data-snap-parallax-x="20%" data-snap-parallax-opacity="-0.2" alt="">
<p class="title" data-snap-parallax-x="20vw" data-snap-parallax-opacity="-2" data-snap-parallax-opacity-scope="-0.5,0.5">Title 2</p>
</div>
</div>
<div class="slide">
<div class="wrap" data-snap-parallax-scale="0.25" data-snap-parallax-scale-abs data-snap-parallax-x="-12.5%" data-snap-parallax-x-scope="-2,2" data-snap-parallax-rotate-y="10deg">
<img class="image" src="https://picsum.photos/id/768/1920/1080" data-snap-parallax-x="20%" data-snap-parallax-opacity="-0.2" alt="">
<p class="title" data-snap-parallax-x="20vw" data-snap-parallax-opacity="-2" data-snap-parallax-opacity-scope="-0.5,0.5">Title 3</p>
</div>
</div>
<div class="slide">
<div class="wrap" data-snap-parallax-scale="0.25" data-snap-parallax-scale-abs data-snap-parallax-x="-12.5%" data-snap-parallax-x-scope="-2,2" data-snap-parallax-rotate-y="10deg">
<img class="image" src="https://picsum.photos/id/832/1920/1080" data-snap-parallax-x="20%" data-snap-parallax-opacity="-0.2" alt="">
<p class="title" data-snap-parallax-x="20vw" data-snap-parallax-opacity="-2" data-snap-parallax-opacity-scope="-0.5,0.5">Title 4</p>
</div>
</div>
</div>
</div>
CSS
.carousel {
position: relative;
width: 100%;
height: 75svh;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 65vw;
transform-style: preserve-3d;
perspective: 100vw;
}
.wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: #000;
border-radius: 0.5rem;
overflow: hidden;
}
.image {
position: absolute;
top: 0;
left: -20%;
width: 140%;
height: 100%;
object-fit: cover;
}
.title {
margin: 0;
position: absolute;
bottom: 1.5rem;
left: 1.5rem;
font-size: 1.5rem;
font-weight: bold;
color: #fff;
}
JavaScript
import {
Snap,
EaseOutExpo
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
origin: "center",
loop: true,
grabCursor: true,
gap: "2,5vw",
lerp: 0.2,
easing: EaseOutExpo,
duration: 1500
});
container.classList.add("ready");
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord,
isVisible
}) => {
element.style.transform = `translate(${coord}px, 0)`;
});
});
Stack overlap
Overlapping stack: the active slide stays in place while previous slides shrink back in Z-space.
Driven by progress — translateX, translateZ, and brightness in onUpdate.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/757/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/59/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/400/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/260/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/358/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/478/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/625/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/356/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/380/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/652/600/900" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(50vw, 50svh);
position: relative;
width: var(--size);
height: calc(var(--size) * 0.675);
opacity: 0;
transition: opacity 0.25s linear;
transform-style: preserve-3d;
perspective: calc(var(--size) * 2.5);
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0px 0px 50px 0px rgba(0, 0, 0, 0.1);
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
clamp
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
origin: "center",
loop: false,
gap: 20,
grabCursor: true
});
carousel.on("update", (data, {
slides
}) => {
slides.forEach(({
element,
size,
progress,
coord
}) => {
const x = progress > 0 ? 0 : coord;
const z = progress < 0 ? 0 : size * progress * -1;
const brightness = clamp(1 - progress * 0.5);
element.style.transform = `translateX(${x}px) translateZ(${z}px)`;
element.style.filter = `brightness(${brightness})`;
});
});
container.classList.add("ready");
Clip-path carousel
Full-page vertical carousel with clip-path, 3D rotation, and asymmetric reveal timing per corner.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img class="image" src="https://picsum.photos/id/608/1920/1080" alt="">
<p class="title">Slide 1</p>
</div>
<div class="slide">
<img class="image" src="https://picsum.photos/id/698/1920/1080" alt="">
<p class="title">Slide 2</p>
</div>
<div class="slide">
<img class="image" src="https://picsum.photos/id/988/1920/1080" alt="">
<p class="title">Slide 3</p>
</div>
<div class="slide">
<img class="image" src="https://picsum.photos/id/788/1920/1080" alt="">
<p class="title">Slide 4</p>
</div>
</div>
</div>
CSS
.carousel {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 0;
transition: opacity 0.25s linear;
transform-style: preserve-3d;
perspective: 100rem;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
will-change: transform, clip-path;
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.title {
margin: 0;
position: relative;
font-size: 1.5rem;
font-weight: bold;
color: #fff;
}
JavaScript
import {
Snap,
EaseOutExpo
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
grabCursor: true,
loop: true,
direction: "vertical",
wheel: true,
followWheel: false,
duration: 1500,
lerp: 0.15
});
container.classList.add("ready");
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord,
progress,
isVisible
}) => {
let translateY = 0;
let clipY1 = 100;
let clipY2 = 100;
let zIndex = 0;
let rot = 0;
let origin = "";
if (progress >= 0 && progress < 1) {
clipY1 = (1 - progress) * 100;
clipY2 = (1 - progress ** 2) * 100;
translateY = progress * -10;
rot = 50 * progress;
zIndex = 2;
origin = "top center";
} else if (progress < 0 && progress > -1) {
clipY1 = (1 - progress) * 100;
translateY = progress * -10;
rot = 50 * progress;
zIndex = 1;
origin = "bottom center";
} else {
translateY = 500;
}
element.style.clipPath = `polygon(0% 0%, 100% 0%, 100% ${clipY1}%, 0% ${clipY2}%)`;
element.style.transform = `translate(0, ${translateY}%) rotateX(${rot}deg)`;
element.style.transformOrigin = origin;
element.style.zIndex = zIndex;
});
});
Fullsize parallax rotation
Vertical full-page carousel with child parallax via data-snap-parallax-* attributes on images and content.
See also Snap parallax. Snap moves slides with translateY(${coord}px); parallax runs on nested elements.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img class="image" data-snap-parallax-y="30%" data-snap-parallax-scale="0.1" data-snap-parallax-scale-abs src="https://picsum.photos/id/608/1920/1080" alt="">
<div class="content" data-snap-parallax-opacity="-1" data-snap-parallax-rotate="-10deg" data-snap-parallax-y="20%" data-snap-parallax-scale="0.05">
<p class="title">Slide 1</p>
<p class="description">Lorem ipsum dolor sit amet</p>
</div>
</div>
<div class="slide">
<img class="image" data-snap-parallax-y="30%" data-snap-parallax-scale="0.1" data-snap-parallax-scale-abs src="https://picsum.photos/id/698/1920/1080" alt="">
<div class="content" data-snap-parallax-opacity="-1" data-snap-parallax-rotate="-10deg" data-snap-parallax-y="20%" data-snap-parallax-scale="0.05">
<p class="title">Slide 2</p>
<p class="description">Lorem ipsum dolor sit amet</p>
</div>
</div>
<div class="slide">
<img class="image" data-snap-parallax-y="30%" data-snap-parallax-scale="0.1" data-snap-parallax-scale-abs src="https://picsum.photos/id/988/1920/1080" alt="">
<div class="content" data-snap-parallax-opacity="-1" data-snap-parallax-rotate="-10deg" data-snap-parallax-y="20%" data-snap-parallax-scale="0.05">
<p class="title">Slide 3</p>
<p class="description">Lorem ipsum dolor sit amet</p>
</div>
</div>
<div class="slide">
<img class="image" data-snap-parallax-y="30%" data-snap-parallax-scale="0.1" data-snap-parallax-scale-abs src="https://picsum.photos/id/788/1920/1080" alt="">
<div class="content" data-snap-parallax-opacity="-1" data-snap-parallax-rotate="-10deg" data-snap-parallax-y="20%" data-snap-parallax-scale="0.05">
<p class="title">Slide 1</p>
<p class="description">Lorem ipsum dolor sit amet</p>
</div>
</div>
<div class="slide">
<img class="image" data-snap-parallax-y="30%" data-snap-parallax-scale="0.1" src="https://picsum.photos/id/608/1920/1080" alt="">
<div class="content" data-snap-parallax-opacity="-1" data-snap-parallax-rotate="-10deg" data-snap-parallax-y="20%" data-snap-parallax-scale="0.05">
<p class="title">Slide 4</p>
<p class="description">Lorem ipsum dolor sit amet</p>
</div>
</div>
</div>
</div>
CSS
.carousel {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
opacity: 0;
transition: opacity 0.25s linear;
transform-style: preserve-3d;
perspective: 100rem;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 0 2rem;
overflow: hidden;
will-change: transform, clip-path;
}
.image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.content {
position: relative;
width: 100%;
height: 100%;
transform-origin: center left;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 0.5rem;
}
.title {
margin: 0;
font-size: 2rem;
font-weight: bold;
color: #fff;
}
.description {
margin: 0;
font-size: 1.25rem;
color: #fff;
}
JavaScript
import {
Snap
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
grabCursor: true,
loop: true,
direction: "vertical",
wheel: true,
followWheel: false,
duration: 1500
});
container.classList.add("ready");
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord
}) => {
element.style.transform = `translateY(${coord}px)`;
});
});
Cards
Card stack with horizontal offset, Z-depth, and rotation from progress. Passed cards tuck beneath the active slide.
Uses onSwipeStart to track the gesture origin slide for asymmetric motion.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide" style="background: var(--gradient-slide-1, #000)">1</div>
<div class="slide" style="background: var(--gradient-slide-2, #000)">2</div>
<div class="slide" style="background: var(--gradient-slide-3, #000)">3</div>
<div class="slide" style="background: var(--gradient-slide-4, #000)">4</div>
<div class="slide" style="background: var(--gradient-slide-5, #000)">5</div>
<div class="slide" style="background: var(--gradient-slide-6, #000)">6</div>
<div class="slide" style="background: var(--gradient-slide-7, #000)">7</div>
</div>
</div>
CSS
.carousel {
position: relative;
width: 10rem;
height: 13rem;
transform-style: preserve-3d;
perspective: 1000px;
opacity: 0;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 1rem;
font-size: 1rem;
font-weight: bold;
color: #fff;
}
JavaScript
import {
Snap,
inRange,
clamp
} from "vevet";
const container = document.getElementById("carousel");
let startIndex = 0;
new Snap({
container: container,
direction: "horizontal",
grabCursor: true
},
{
onResize: () => {
container.classList.add("ready");
},
onSwipeStart: (data, {
activeIndex
}) => {
startIndex = activeIndex;
},
onUpdate: (data, {
slides
}) => {
const baseRotation = 6;
const baseX = 12;
const baseZ = -100;
const maxVisible = 6;
slides.forEach(({
element,
index,
progress
}) => {
let x = -baseX * progress;
let rotate = progress * -baseRotation;
const z = Math.abs(progress) * baseZ;
if (inRange(progress, -1, 1)) {
if (startIndex === index) {
x += Math.sin(Math.PI * progress) * -60;
} else {
x += Math.sin(Math.PI * progress) * -20;
}
}
x = clamp(x, -baseX * maxVisible, baseX * maxVisible);
rotate = clamp(
rotate,
-baseRotation * maxVisible,
baseRotation * maxVisible);
element.style.transform = `translateX(${x}%) translateZ(${z}px) rotate(${rotate}deg)`;
});
}
});
Slick gap
Exponential gap between slides based on progress — each slide gets an extra offset: coord + progress * absGap.
Uses freemode: "sticky", origin: "center", and shortSwipes: false.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/757/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/59/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/400/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/260/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/358/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/478/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/625/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/356/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/380/600/900" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/652/600/900" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(40vw, 40svh);
position: relative;
width: 100%;
height: var(--size);
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: calc(var(--size) * 0.6);
height: var(--size);
border-radius: 16px;
overflow: hidden;
box-shadow: 0px 0px 50px 0px rgba(0, 0, 0, 0.1);
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
vevet
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
grabCursor: true,
origin: "center",
loop: true,
freemode: "sticky",
shortSwipes: false
});
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord,
progress
}) => {
const absGap = Math.abs(progress) * -20;
const gap = progress * absGap;
element.style.transform = `translateX(${coord + gap}px)`;
});
});
container.classList.add("ready");
Android UI
Android home-screen layout with two Snap instances — page dots and a 3D active-apps dock.
HTML
<div class="fullpage-center">
<div class="phone">
<div class="main">
<img class="wallpaper" src="https://vevetjs.com/shared/android-ui-snap/wallpaper.png">
<div class="pages">
<div class="grid">
<img src="https://vevetjs.com/shared/android-ui-snap/settings.png">
<img src="https://vevetjs.com/shared/android-ui-snap/clock.png">
<img src="https://vevetjs.com/shared/android-ui-snap/calendar.png">
<img src="https://vevetjs.com/shared/android-ui-snap/calculator.png">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<img src="https://vevetjs.com/shared/android-ui-snap/google.png">
<img src="https://vevetjs.com/shared/android-ui-snap/maps.png">
<img src="https://vevetjs.com/shared/android-ui-snap/drive.png">
<img src="https://vevetjs.com/shared/android-ui-snap/photos.png">
<img src="https://vevetjs.com/shared/android-ui-snap/meet.png">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<img src="https://vevetjs.com/shared/android-ui-snap/music.png">
<img src="https://vevetjs.com/shared/android-ui-snap/beats.png">
</div>
<div class="grid">
<img src="https://vevetjs.com/shared/android-ui-snap/telegram.png">
<img src="https://vevetjs.com/shared/android-ui-snap/linkedin.png">
<div></div>
<div></div>
<img src="https://vevetjs.com/shared/android-ui-snap/instagram.png">
<img src="https://vevetjs.com/shared/android-ui-snap/chrome.png">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<img src="https://vevetjs.com/shared/android-ui-snap/Gallery.png">
<img src="https://vevetjs.com/shared/android-ui-snap/Files.png">
<img src="https://vevetjs.com/shared/android-ui-snap/camera.png">
</div>
<div class="grid"></div>
</div>
<ul class="pages-nav">
<li>
<button type="button" aria-label="Page 1" class="pages-nav__button"></button>
</li>
<li>
<button type="button" aria-label="Page 1" class="pages-nav__button"></button>
</li>
<li>
<button type="button" aria-label="Page 1" class="pages-nav__button"></button>
</li>
</ul>
</div>
<div class="active-apps">
<div class="active-apps__slide">
<img class="active-apps__icon" src="https://vevetjs.com/shared/android-ui-snap/clock.png">
<img class="active-apps__preview" src="https://vevetjs.com/shared/android-ui-snap/clock.jpg">
</div>
<div class="active-apps__slide">
<img class="active-apps__icon" src="https://vevetjs.com/shared/android-ui-snap/settings.png">
<img class="active-apps__preview" src="https://vevetjs.com/shared/android-ui-snap/settings.jpg">
</div>
<div class="active-apps__slide">
<img class="active-apps__icon" src="https://vevetjs.com/shared/android-ui-snap/Files.png">
<img class="active-apps__preview" src="https://vevetjs.com/shared/android-ui-snap/files.jpg">
</div>
<div class="active-apps__slide">
<img class="active-apps__icon" src="https://vevetjs.com/shared/android-ui-snap/calendar.png">
<img class="active-apps__preview" src="https://vevetjs.com/shared/android-ui-snap/calendar.jpg">
</div>
</div>
<div class="navbar"></div>
</div>
</div>
CSS
img {
background: transparent;
}
.phone {
--size: 85svh;
position: relative;
width: calc(0.4617 * var(--size));
height: var(--size);
border: calc(0.012 * var(--size)) solid #000;
box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.5);
border-radius: calc(0.04 * var(--size));
overflow: hidden;
}
.main {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pages-nav {
position: absolute;
bottom: 15.8%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: calc(0.01328125 * var(--size));
margin: 0;
padding: 0;
list-style-type: none;
}
.pages-nav__button {
display: block;
padding: 0;
border: 0;
width: calc(0.0078125 * var(--size));
height: calc(0.0078125 * var(--size));
border-radius: 50%;
opacity: 0.5;
transition: opacity 0.125s linear;
background: #fff;
&.active {
opacity: 1;
}
}
.pages {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.grid {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 100%;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: calc(0.03 * var(--size)) calc(0.023 * var(--size));
padding: calc(0.096 * var(--size)) calc(0.03 * var(--size));
img {
width: 100%;
height: auto;
}
div {
aspect-ratio: 1;
}
}
.wallpaper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.active-apps {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgb(0 0 0 / 50%);
transform-style: preserve-3d;
perspective: var(--size);
opacity: 0;
pointer-events: none;
&.active {
pointer-events: all;
}
}
.active-apps__slide {
position: absolute;
top: calc(0.144 * var(--size));
left: 0;
width: calc(0.2912 * var(--size));
height: calc(0.5965 * var(--size));
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.active-apps__preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-position: cover;
border-radius: calc(0.022 * var(--size));
}
.active-apps__icon {
position: absolute;
z-index: 1;
top: 0;
left: 50%;
width: 20%;
height: auto;
transform: translate(-50%, -50%);
}
.navbar {
position: absolute;
bottom: calc(-0.023437 * var(--size));
bottom: 0;
left: 50%;
width: calc(0.16 * var(--size));
height: calc(0.0359375 * var(--size));
transform: translateX(-50%);
cursor: grab;
&::before {
content: "";
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: calc(0.0046875 * var(--size));
margin-top: calc(0.0046875 * var(--size));
border-radius: 3px;
opacity: 0.5;
background: #fff;
animation: navbar-pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
}
}
@keyframes navbar-pulse {
from {
transform: scale(1) translateY(0%);
}
50% {
transform: scale(1.1) translateY(-100%);
}
to {
transform: scale(1) translateY(0%);
}
}
JavaScript
import {
Snap,
lerp,
clamp,
scoped,
EaseInOutSine,
Swipe,
addEventListener,
easing,
Timeline,
clampScope,
EaseOutSine
} from "vevet";
const activeAppsContainer = document.querySelector(".active-apps");
const navbarContainer = document.querySelector(".navbar");
const pagesContainer = document.querySelector(".pages");
const pages = new Snap({
container: pagesContainer,
direction: "horizontal"
});
pages.on("update", () => {
pages.slides.forEach(({
element,
coord
}) => {
element.style.transform = `translateX(${coord}px)`;
});
});
const pagesDots = document.querySelectorAll(".pages-nav__button");
pagesDots[0].classList.add("active");
pages.on("activeSlide", () => {
pagesDots.forEach((dot, index) => {
dot.classList.toggle("active", index === pages.activeIndex);
});
});
///
const activeApps = new Snap({
container: activeAppsContainer,
direction: "horizontal",
gap: "2.8vh",
origin: "center",
freemode: "sticky",
shortSwipes: false,
swipeInertiaRatio: 0.1,
activeIndex: activeAppsContainer.children.length - 1
});
const activeAppsIcons = activeApps.slides.map((slide) =>
slide.element.querySelector(".active-apps__icon"));
activeApps.on("update", () => renderActiveApps(1));
function renderActiveApps(progress, parallax = 0) {
const activeSlideCoords = getActiveAppsCoords(activeApps.activeSlide);
activeApps.slides.forEach(slide => {
const targetCoords = getActiveAppsCoords(slide);
const x = lerp(activeSlideCoords.x, targetCoords.x, progress) + parallax;
const z = lerp(activeSlideCoords.z, targetCoords.z, progress);
const scale = lerp(activeSlideCoords.scale, targetCoords.scale, progress);
const rotate = lerp(
activeSlideCoords.rotate,
targetCoords.rotate,
progress);
const iconOpacity = 1 - clamp(Math.abs(slide.progress));
const iconScale = lerp(0.75, 1.01, iconOpacity);
const iconY = lerp(40, 0, iconOpacity);
const icon = activeAppsIcons[slide.index];
slide.element.style.transform = `translate3d(${x}px, 0, ${z}px) scale(${scale}) rotateY(${rotate}deg)`;
icon.style.transform = `scale(${iconScale}) translate(-50%, ${
-50 - iconY
}%)`;
icon.style.opacity = `${iconOpacity}`;
});
}
function getActiveAppsCoords({
coord,
progress,
size
}) {
let scale = 1;
let x = coord;
let z = 0;
let rotate = 0;
if (progress < 0) {
const scaleProgress = clamp(Math.abs(progress), 0, 1);
const targetScale = 1.0497;
scale = 1 + (targetScale - 1) * scaleProgress;
} else {
x += size * progress * 0.725;
z += size * progress * -0.35;
rotate = progress * 10;
}
return {
scale,
x,
z,
rotate
};
}
///
let showActiveAppsTm;
let hideActiveAppsTm;
let isAactiveAppsVisible = false;
function showActiveApps() {
if (showActiveAppsTm || isAactiveAppsVisible || hideActiveAppsTm) {
return;
}
activeAppsContainer.classList.add("active");
isAactiveAppsVisible = true;
showActiveAppsTm = new Timeline({
duration: 650
});
showActiveAppsTm.on("update", ({
progress
}) => {
const showProgress = clampScope(progress, [0, 0.5]);
const appsProgress = clampScope(progress, [0.3, 1]);
pagesContainer.style.transform = `scale(${1 - 0.05 * progress})`;
pagesContainer.style.opacity = `${1 - progress}`;
activeAppsContainer.style.backdropFilter = `blur(${showProgress * 10}px)`;
activeAppsContainer.style.opacity = `${showProgress}`;
const appsParallax = lerp(-20, 0, easing(appsProgress, EaseOutSine));
renderActiveApps(easing(appsProgress, EaseOutSine), appsParallax);
});
showActiveAppsTm.on("end", () => {
showActiveAppsTm = undefined;
});
showActiveAppsTm.play();
}
function hideActiveApps() {
if (!isAactiveAppsVisible || showActiveAppsTm) {
return;
}
activeAppsContainer.classList.remove("active");
isAactiveAppsVisible = false;
hideActiveAppsTm = new Timeline({
duration: 200
});
hideActiveAppsTm.on("update", ({
progress
}) => {
const showProgress = 1 - progress;
pagesContainer.style.transform = `scale(${0.95 + 0.05 * progress})`;
pagesContainer.style.opacity = `${progress}`;
activeAppsContainer.style.backdropFilter = `blur(${showProgress * 10}px)`;
activeAppsContainer.style.opacity = `${showProgress}`;
});
hideActiveAppsTm.on("end", () => {
hideActiveAppsTm = undefined;
activeApps.toSlide(activeApps.slides.length - 1, {
duration: 0
});
});
hideActiveAppsTm.play();
}
///
const activeAppsOutsideClick = addEventListener(
activeAppsContainer,
"click",
evt => {
const {
target
} = evt;
const isSlide = activeApps.slides.some(({
element
}) =>
element?.contains(target));
if (!isSlide) {
hideActiveApps();
}
});
///
const navbarSwipe = new Swipe({
container: navbarContainer,
axis: "y"
});
navbarSwipe.on("move", ({
diff
}) => {
const max = -50;
const swipeProgress = easing(clamp(diff.y / max, 0, 1), EaseInOutSine);
const y = swipeProgress * max;
const scale = 1 + swipeProgress * 0.1;
navbarContainer.style.transition = "none";
navbarContainer.style.transform = `translate(-50%, ${y}%) scale(${scale})`;
});
navbarSwipe.on("end", ({
diff
}) => {
navbarContainer.style.transition = "transform 0.35s ease-out";
navbarContainer.style.transform = "translate(-50%, 0)";
if (diff.y < -20) {
if (isAactiveAppsVisible) {
hideActiveApps();
} else {
showActiveApps();
}
}
});
3D stack
Vertical 3D stack: previous slides fold beneath the active one with rotateX, skew, and opacity from progress.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/170/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/190/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/230/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/260/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/270/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/280/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/330/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/340/1920/1080" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(80vw, 80svh);
position: relative;
width: var(--size);
height: calc(var(--size) * 0.7);
transform-style: preserve-3d;
perspective: 1000px;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: 100%;
height: 100%;
border-radius: 1rem;
overflow: hidden;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
inRange,
lerp
} from "vevet";
const container = document.getElementById("carousel");
const ampX = 0.2;
const ampY = 0.6;
const ampZ = 0.5;
const rotateYAmp = -35;
const carousel = new Snap({
container,
direction: "vertical",
grabCursor: true,
wheel: true,
loop: true
});
carousel.on("update", () => {
carousel.slides.forEach(({
element,
progress,
size
}) => {
let opacity = 0;
let y = 0;
let x = 0;
let z = 0;
let rotateX = 0;
let rotateZ = 0;
let brightness = 1;
let skew = 0;
if (inRange(progress, 0, 1)) {
const sine = Math.sin(Math.PI * progress);
y = sine * size * ampY * -1;
x = sine * size * ampX * -1;
z = progress * size * ampZ * -1;
rotateX = progress * 180;
rotateZ = sine * rotateYAmp;
opacity = 1;
brightness = lerp(1, 0.25, progress);
skew = lerp(0, 10, sine);
} else if (inRange(progress, -1, 0)) {
z = progress * size * ampZ;
opacity = 1;
}
element.style.opacity = `${opacity}`;
element.style.transform = `translate3d(${x}px, ${y}px, ${z}px) rotateX(${rotateX}deg) rotateZ(${rotateZ}deg) skew(${skew}deg)`;
element.style.filter = `brightness(${brightness})`;
});
});
container.classList.add("ready");
3D stack 2
Variant of the vertical stack — upcoming slides stay partially visible below the active slide.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/170/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/190/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/230/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/260/1920/1080" alt="">
</div>
</div>
</div>
CSS
.carousel {
--size: min(80vw, 80svh);
position: relative;
width: var(--size);
height: calc(var(--size) * 0.7);
transform-style: preserve-3d;
perspective: calc(var(--size) * 1.5);
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: 100%;
height: 100%;
border-radius: 1rem;
overflow: hidden;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
JavaScript
import {
Snap,
inRange,
lerp,
clamp
} from "vevet";
const container = document.getElementById("carousel");
const ampY = 0.5;
const ampZ = 0.6;
const carousel = new Snap({
container,
direction: "vertical",
grabCursor: true,
wheel: true
});
carousel.on("update", () => {
carousel.slides.forEach(({
element,
progress,
size
}) => {
let opacity = 0;
let y = 0;
let z = 0;
let rotateX = 0;
let brightness = 1;
if (inRange(progress, 0, 1)) {
const sine = Math.sin(Math.PI * progress);
y = sine * size * ampY * -1;
z = progress * size * ampZ * -1;
rotateX = progress * 180;
opacity = 1;
brightness = lerp(1, 0.25, progress);
} else if (inRange(progress, -Infinity, 0)) {
y = progress * size * -0.2;
z = progress * size * ampZ;
opacity = 1;
brightness = clamp(1 - Math.abs(progress) * 0.25);
rotateX = progress * 5;
}
element.style.opacity = `${opacity}`;
element.style.transform = `translate3d(0, ${y}px, ${z}px) rotateX(${rotateX}deg)`;
element.style.filter = `brightness(${brightness})`;
});
});
container.classList.add("ready");
TimeSelect
Vertical time-picker with dynamically created slides, 3D rotateX, and opacity fade by distance from center.
HTML
<div class="fullpage-center">
<div class="selector">
<div class="carousel" id="hours"></div>
:
<div class="carousel" id="minutes"></div>
</div>
</div>
CSS
.selector {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1rem;
}
.carousel {
position: relative;
width: 3rem;
height: 2.25rem;
opacity: 0;
transition: opacity 0.25s linear;
transform-style: preserve-3d;
perspective: 4rem;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
JavaScript
import {
Snap,
clamp,
scoped
} from "vevet";
function createSelect(container, max) {
for (let i = 0; i < max; i += 1) {
const slide = document.createElement("div");
slide.classList.add("slide");
container.append(slide);
slide.innerHTML = `${i}`.padStart(2, "0");
}
const carousel = new Snap({
container,
gap: 0,
direction: "vertical",
loop: true,
origin: "center",
freemode: "sticky",
wheel: true,
grabCursor: true,
swipeInertiaRatio: 0.6,
shortSwipes: false
});
container.classList.add("ready");
carousel.on("update", () => {
carousel.slides.forEach(({
element,
coord,
isVisible,
progress
}) => {
const rotateX = progress * 20;
const z = Math.abs(progress) * -0.5;
const opacity = 1 - clamp(Math.abs(scoped(progress, 0, 2.5)));
element.style.transform = `translate(0, ${coord}px) rotateX(${rotateX}deg) translateZ(${z}rem)`;
element.style.opacity = opacity;
});
});
}
createSelect(document.getElementById("hours"), 24);
createSelect(document.getElementById("minutes"), 60);
Parallax glide
Horizontal carousel with a glide parallax offset: coord + parallax * (size / 2) where parallax = clamp(progress, 0, 1).
Includes dot navigation via carousel.toSlide(index) and the activeSlide callback.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/940/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/950/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/960/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/970/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/980/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/990/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/1000/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/1020/1920/1080" alt="">
</div>
</div>
<ul class="dots">
<li>
<button type="button" class="dot" title="Slide 1"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 2"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 3"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 4"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 5"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 6"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 7"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 8"></button>
</li>
</ul>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.carousel {
position: relative;
width: 100%;
height: 50svh;
min-height: 300px;
overflow: hidden;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: 100%;
height: 100%;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.dots {
padding: 0;
margin: 0;
list-style-type: none;
display: flex;
gap: 10px;
}
.dot {
padding: 0;
cursor: pointer;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #fff;
border: 2px solid #000;
transition: border-color 0.35s linear, background-color 0.35s linear;
}
.dot.active {
background-color: #000;
border-color: #fff;
}
JavaScript
import {
Snap,
vevet,
clamp
} from "vevet";
const container = document.getElementById("carousel");
const carousel = new Snap({
container,
direction: "horizontal",
grabCursor: true,
wheel: true,
wheelAxis: "y"
});
carousel.on("update", () => {
carousel.slides.forEach(({
element,
size,
coord,
progress
}) => {
const parallax = clamp(progress, 0, 1);
const x = coord + parallax * (size / 2);
element.style.transform = `translateX(${x}px)`;
});
});
container.classList.add("ready");
const dots = document.querySelectorAll(".dot");
dots.forEach((dot, index) => {
dot.addEventListener("click", () => {
carousel.toSlide(index);
});
});
function updateDots() {
dots.forEach((dot, index) => {
dot.classList.toggle("active", index === carousel.activeIndex);
});
}
carousel.on("activeSlide", updateDots);
updateDots();
Simple fade
Opacity-only transitions: opacity = clamp(1 - Math.abs(progress)) with followSwipe: false so slides cross-fade instead of tracking the finger.
HTML
<div class="fullpage-center">
<div class="carousel" id="carousel">
<div class="slide">
<img src="https://picsum.photos/id/1070/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/1080/1920/1080" alt="">
</div>
<div class="slide">
<img src="https://picsum.photos/id/1082/1920/1080" alt="">
</div>
</div>
<ul class="dots">
<li>
<button type="button" class="dot" title="Slide 1"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 2"></button>
</li>
<li>
<button type="button" class="dot" title="Slide 3"></button>
</li>
</ul>
</div>
CSS
.fullpage-center {
padding: 1rem;
}
.carousel {
position: relative;
width: 100%;
height: 50svh;
min-height: 300px;
opacity: 0;
transition: opacity 0.25s linear;
}
.carousel.ready {
opacity: 1;
}
.slide {
position: absolute;
width: 100%;
height: 100%;
transition: opacity 0.35s linear;
opacity: 0;
}
.slide.active {
opacity: 1;
}
.slide img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.dots {
padding: 0;
margin: 0;
list-style-type: none;
display: flex;
gap: 10px;
}
.dot {
padding: 0;
cursor: pointer;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #fff;
border: 2px solid #000;
transition: border-color 0.35s linear, background-color 0.35s linear;
}
.dot.active {
background-color: #000;
border-color: #fff;
}
JavaScript
import {
Snap,
vevet,
clamp
} from "vevet";
const container = document.getElementById("carousel");
const dots = document.querySelectorAll(".dot");
const carousel = new Snap({
container,
duration: 0,
followSwipe: false,
loop: true
});
container.classList.add("ready");
dots.forEach((dot, index) => {
dot.addEventListener("click", () => {
carousel.toSlide(index);
});
});
function onActiveSlide() {
carousel.slides.forEach(({
element,
index
}) => {
element.classList.toggle("active", index === carousel.activeIndex);
});
dots.forEach((dot, index) => {
dot.classList.toggle("active", index === carousel.activeIndex);
});
}
carousel.on("activeSlide", onActiveSlide);
onActiveSlide();