Scroll Snap Gallery — Free HTML CSS JS Snippet
Scroll Snap Gallery · Scroll · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="scene">
<div class="gallery" id="gallery">
<div class="slide" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">
<div class="slide-num">01</div>
<h3>Design Systems</h3>
<p>Building consistent UI at scale</p>
</div>
<div class="slide" style="background:linear-gradient(135deg,#ec4899,#f97316)">
<div class="slide-num">02</div>
<h3>Motion Design</h3>
<p>Meaningful animations that guide users</p>
</div>
<div class="slide" style="background:linear-gradient(135deg,#0ea5e9,#10b981)">
<div class="slide-num">03</div>
<h3>Typography</h3>
<p>Type hierarchy, scale, and rhythm</p>
</div>
<div class="slide" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">
<div class="slide-num">04</div>
<h3>Color Theory</h3>
<p>Palettes, contrast, and accessibility</p>
</div>
<div class="slide" style="background:linear-gradient(135deg,#8b5cf6,#0ea5e9)">
<div class="slide-num">05</div>
<h3>Layout & Grid</h3>
<p>Spatial harmony and visual structure</p>
</div>
</div>
<div class="dots" id="dots"></div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #0f172a; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
.scene { display: flex; flex-direction: column; align-items: center; gap: 16px; width: 100%; max-width: 440px; }
.gallery {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
gap: 12px;
width: 100%;
padding-bottom: 4px;
scrollbar-width: none;
}
.gallery::-webkit-scrollbar { display: none; }
.slide {
flex-shrink: 0;
width: 100%;
scroll-snap-align: start;
border-radius: 18px;
padding: 36px 28px;
display: flex; flex-direction: column; gap: 10px;
color: #fff; min-height: 220px;
justify-content: flex-end;
}
.slide-num { font-size: 11px; font-weight: 700; opacity: 0.6; letter-spacing: 1px; }
h3 { font-size: 24px; font-weight: 800; line-height: 1.2; }
p { font-size: 14px; opacity: 0.8; }
.dots { display: flex; gap: 6px; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: #334155; cursor: pointer; transition: background 0.2s, width 0.2s; }
.dot.active { background: #6366f1; width: 20px; border-radius: 6px; }const gallery = document.getElementById('gallery');
const dotsEl = document.getElementById('dots');
const slides = gallery.querySelectorAll('.slide');
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot' + (i === 0 ? ' active' : '');
d.onclick = () => gallery.scrollTo({ left: slides[i].offsetLeft, behavior: 'smooth' });
dotsEl.appendChild(d);
});
const dots = dotsEl.querySelectorAll('.dot');
gallery.addEventListener('scroll', () => {
const idx = Math.round(gallery.scrollLeft / gallery.clientWidth);
dots.forEach((d, i) => d.classList.toggle('active', i === idx));
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Scroll Snap Gallery — scroll-snap-type, scroll-snap-align & IntersectionObserver Dots

A scroll snap gallery provides smooth, paginated horizontal scrolling where each slide snaps into view — like an image carousel or coverflow carousel but driven by native browser scroll rather than JavaScript animation. Wire each slide to a photo gallery or lightbox for full-size viewing. It works naturally with touch swipe on mobile and mouse drag on desktop.
The CSS scroll snap
.gallery { scroll-snap-type: x mandatory; overflow-x: auto; scroll-behavior: smooth } sets the container as a horizontal snap container. mandatory means the browser will always snap to a snap point after scrolling stops — it will never rest between slides. Each .slide { scroll-snap-align: center } declares each slide as a snap target.
The dot navigation
Clicking a dot calls gallery.scrollTo({ left: slides[i].offsetLeft, behavior: 'smooth' }). offsetLeft is the slide's distance from its parent container left edge — scrolling to this position aligns the slide's left edge with the container's left edge.
IntersectionObserver active dot
An IntersectionObserver with threshold: 0.5 watches each slide. When a slide is more than 50% visible, its corresponding dot gets the .active class. root: gallery scopes the observer to the gallery container rather than the viewport. This automatically updates the active dot when the user scrolls manually.
Hiding the scrollbar
scrollbar-width: none (Firefox) and ::-webkit-scrollbar { display: none } (Chrome/Safari) hide the scrollbar, giving the gallery a clean appearance. Scroll interaction still works; only the visual scrollbar is hidden.
How CSS scroll snap works
The gallery container has scroll-snap-type: x mandatory on the x-axis. Each card has scroll-snap-align: start. When the user scrolls and releases, the browser automatically snaps the scroll position to the nearest card's start edge. mandatory means the snap always happens — optional allows free scrolling between snaps. The scroll uses overflow-x: auto and a hidden scrollbar via ::-webkit-scrollbar { display: none }.
The dot indicator sync
A scroll event listener reads scrollLeft / scrollWidth * numCards to compute the approximate active index. It then updates the dot indicators — the active dot gets the filled style. requestAnimationFrame is used to throttle the update to one per paint. For more accurate snap detection, use IntersectionObserver on each card with a 0.5 threshold.
Keyboard navigation
The left/right arrow buttons call scrollBy with the card width and smooth behaviour: container.scrollBy({ left: cardWidth, behavior: 'smooth' }). Prev/next buttons disable at the boundaries when scroll position is at 0 or maximum. Keyboard arrow keys can also be wired to the same scrollBy calls for accessibility.
Step by step
How to Use
- 1Scroll or click dotsScroll horizontally or click the dots to navigate. Each slide snaps into position. Swipe on mobile.
- 2Update slide contentIn the HTML panel, change the gradient background, emoji, title, and description in each .slide div.
- 3Add more slidesCopy a .slide div and paste it inside .gallery. A new dot is generated automatically via the JS querySelectorAll forEach.
- 4Change snap alignmentUpdate scroll-snap-align: center to start or end on .slide in the CSS.
- 5Change the snap strictnessChange scroll-snap-type: x mandatory to x proximity for snapping only when near a snap point.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
scroll-snap-type on a scroll container declares it as a snap context. scroll-snap-align on children declares each as a snap point. The browser snaps to the nearest (proximity) or always (mandatory) snap point after scrolling stops.
mandatory snaps after every scroll — the gallery never rests between slides. proximity only snaps when the scroll position is close enough to a snap point. mandatory is better for full-slide carousels; proximity is better for partially-visible item lists.
Each slide is observed with threshold: 0.5 and root: gallery. When a slide is more than 50% within the gallery viewport, the observer fires with isIntersecting: true and the matching dot gets .active.
Copy a .slide div and paste it inside .gallery. The JS querySelectorAll(".slide") picks it up and creates a new dot automatically. The IntersectionObserver observes all slides via querySelectorAll as well.
Yes. CSS scroll snapping is supported natively on all modern mobile browsers. Touch swipe triggers the native scroll which then snaps to the nearest slide.
Yes. Click "JSX" for a React component. The CSS scroll snap properties work identically in React. Use useRef on the gallery container and useEffect with an IntersectionObserver to manage the active dot state.