tool / css scroll snapno library needed
CSS scroll snap generator
Pick the axis, how strict the snap is, which edge lines up, the padding and the sizes, then swipe the preview: it runs the CSS you copy. Take the HTML, the CSS and a few lines of JavaScript for Previous and Next buttons, or the same layout snapped by scrollSnap() with your own duration and easing.
- Properties
- 5
- Layouts
- 3
- Files out
- 3
- scrollSnap
- 1.9 KB
Make a scroll snap container
Start from a layout, then change anything. The hatched bands are the scroll-padding and the red line is the snap line; under the box, each item’s snap position along the scroll, with a marker where the box is now.
preview · the generated CSS, as it isswipe, scroll, or focus it and use the arrow keys
- snapped
- —
- scrollLeft
- —
- read on
- scrollend
No library: scroll-snap-type on the box and scroll-snap-align on its children do the snapping. snap.js is only for the buttons.
<div class="snap" tabindex="0" role="region" aria-label="Carousel">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
</div>
<button type="button" data-snap="prev">Previous</button>
<button type="button" data-snap="next">Next</button>.snap {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 16px;
}
.snap > * {
flex: 0 0 80%;
scroll-snap-align: start;
}const snap = document.querySelector('.snap');
const items = [...snap.children];
// Each item's snap position: the scroll offset that puts its start edge
// on the start of the snapport, clamped to how far the box can scroll.
function positions() {
const box = snap.getBoundingClientRect();
const css = getComputedStyle(snap);
const pad = (side) => parseFloat(css['scrollPadding' + side]) || 0;
const line = box.left + snap.clientLeft + pad('Left');
const max = snap.scrollWidth - snap.clientWidth;
return items.map((item) => {
const r = item.getBoundingClientRect();
return Math.min(max, Math.max(0, snap.scrollLeft + r.left - line));
});
}
// The next snap position past this one, or the last before it.
function go(step) {
const now = snap.scrollLeft;
const all = positions();
const to = step > 0 ? all.find((p) => p > now + 1) : all.findLast((p) => p < now - 1);
if (to === undefined) return;
snap.scrollTo({ left: to, behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
}
document.querySelector('[data-snap="prev"]').addEventListener('click', () => go(-1));
document.querySelector('[data-snap="next"]').addEventListener('click', () => go(1));How scroll snap works
Two properties do the work. scroll-snap-type on the scroll container turns snapping on for an axis, and scroll-snap-align on each child says which of its edges should land on the snap line. That line is the start, centre or end of the snapport, the box’s visible area minus its scroll-padding (fig. 1). For each child the browser works out the scroll offset that puts its edge on the line, a snap position, clamped to what the box can actually scroll; when a scroll ends, it settles on one of them.
.snap {
display: flex;
gap: 16px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline: 16px;
}
.snap > * {
flex: 0 0 80%;
scroll-snap-align: start;
}html, not body, and the alignment on sections as tall as the window.html {
scroll-snap-type: y mandatory;
}
section {
height: 100vh;
scroll-snap-align: start;
}Every scroll snap property
Two go on the box that scrolls and three on its children. Each has longhands per side or per axis, such as scroll-padding-top or scroll-margin-inline.
| Property | Goes on | Values | What it does |
|---|---|---|---|
| scroll-snap-type | the scroll container | none, or an axis (x, y, block, inline, both) then mandatory or proximity | Turns snapping on for that axis. With no second word it is proximity. |
| scroll-padding | the scroll container | a length per side: scroll-padding-inline, -block, -top… | Insets the snapport from the edges of the box, so items line up clear of a sticky header or a peeking edge. It adds no space. scrollIntoView() and #anchor jumps use it too. |
| scroll-snap-align | each child | none, start, center, end: one value for both axes, or block then inline | Which edge of the child lands on the snap line. none, the default, never snaps. |
| scroll-snap-stop | each child | normal, always | With always, a scroll that would pass this child stops on it, so a fast swipe cannot skip it. |
| scroll-margin | each child | a length per side | Grows the child’s snap area by that much, so it lands that far from the line without the child moving. |
CSS scroll snap or scrollSnap()
CSS is the right default. It needs no script, it uses the browser’s own feel on touch screens, the arrow keys work, and it lines up any edge on either axis. svg-scroll-draw’s scrollSnap is for when the snap is part of the design: a glide of your length and curve, a threshold you choose, a callback when it lands, and snapTo() to drive it from buttons or a menu.
| Aspect | CSS scroll snap | scrollSnap() |
|---|---|---|
| Needs | Nothing but CSS: scroll-snap-type on the box, scroll-snap-align on its children. | A script: svg-scroll-draw/snap, 1.9 KB gzipped. Until it runs, the page scrolls freely. |
| The snap motion | The browser’s own. No property sets its speed or its curve. | A glide over duration (600 ms) with easing ('ease-in-out'), or any function of t. |
| When it snaps | When the scroll ends, to the snap position the browser picks: after a fast swipe that can be several items along, unless scroll-snap-stop: always. | 100 ms after the last scroll event: to the next or previous section once the scroll passed threshold (0.3) of the current one, back to it otherwise. One section per gesture. |
| What lines up | The start, centre or end of each child, with scroll-padding and scroll-margin. | Each section’s start edge, on the start of the box or the window. |
| Item sizes | Any: cards narrower than the box, mixed sizes. | Sections that fill the box. Smaller ones near the end can never reach its start. |
| Axes | x, y or both. | One: direction 'vertical' (the default) or 'horizontal'. |
| Knowing where it is | scrollsnapchange in Chromium browsers (version 129 on); elsewhere scrollend and your own sums. | onSnap(index) after every snap, and getCurrentIndex(). |
| Keyboard | The arrow keys go to the next snap position. | Page Down reaches the next section. An arrow press moves too little to pass the threshold, so it glides back. |
| Reduced motion | Up to the browser. | Jumps straight to the section instead of gliding (respectReducedMotion, true by default). |
Its options, each with its default. The instance it returns has snapTo(index), which glides to a section (the index is kept within the sections), getCurrentIndex() and destroy().
| Option | Default | What it does |
|---|---|---|
| direction | 'vertical' | 'vertical' or 'horizontal': the axis it watches and glides along. |
| duration | 600 | How long the glide to a section takes, in milliseconds. |
| easing | 'ease-in-out' | The glide’s curve: 'linear', 'ease-in', 'ease-out', 'ease-in-out', 'spring', 'bounce', 'elastic', or any function of t. A name it does not know falls back to 'ease-in-out'. |
| threshold | 0.3 | How far from the current section a scroll must end to move on, as a fraction of that section’s height or width. Less glides back. |
| scrollContainer | the window | A selector or an element to snap inside, instead of the page. |
| onSnap | none | Called with the section’s index after each snap. |
| respectReducedMotion | true | For readers who ask their system for less motion, jump to the section instead of gliding. It still snaps. |
import { scrollSnap } from 'svg-scroll-draw/snap';
// Full-height <section>s on the page itself: no scrollContainer, so it
// snaps the window. No scroll-snap-type on html, and no smooth scrolling.
const snap = scrollSnap('section', {
duration: 800,
easing: 'ease-out',
onSnap: (index) => console.log('section', index),
});
// Drive it from a menu: glide to the third section.
document.querySelector('#to-pricing').addEventListener('click', () => snap.snapTo(2));
// snap.getCurrentIndex() is the section it last landed on, and
// snap.destroy() stops snapping and removes its listener.scroll-snap-type too: the browser would snap to its own positions while scrollSnap glides to its. Keep scroll-behavior: auto on it as well (on html for the window). scrollSnap sets the scroll position every frame, and smooth scrolling turns each of those into a separate glide, so onSnap fires long before it lands.Scroll snap questions
How do I make a carousel with CSS scroll snap?
Give the row display: flex, overflow-x: auto and scroll-snap-type: x mandatory, and give each card a width (flex: 0 0 80% leaves a slice of the next one showing) and scroll-snap-align: start. A swipe, the wheel or the arrow keys then always come to rest with a card lined up. scroll-padding-inline moves the line they line up on. The carousel preset above writes all of it, with Previous and Next buttons.
What is the difference between mandatory and proximity?
mandatory always comes to rest on a snap position when a scroll ends. proximity snaps only when the scroll ends close to one and otherwise leaves it where it stopped; how close is up to the browser. scroll-snap-type: x on its own means x proximity, so write mandatory if every swipe should land on an item. In both, an item bigger than the box can still be scrolled through.
Why is my CSS scroll snap not working?
Most often scroll-snap-type is on an element that does not scroll. It belongs on the scroll container: the element with overflow: auto or scroll whose content is bigger than it, so a vertical one needs a height. Then check that each child has scroll-snap-align (its default, none, never snaps), that the axis matches the way it scrolls, and, to snap the whole page, that the snap type is on html rather than body.
How do I stop a fast swipe skipping items?
Give the children scroll-snap-stop: always. A scroll that would pass one stops on it instead, so a fling moves one item at a time. normal, the default, lets a fast swipe fly past several and land on a later one. Every current browser supports it.
Can I change the speed or easing of scroll snap?
Not in CSS. The glide into place after a swipe is the browser’s own, and scroll-behavior: smooth only changes scrolls that a script or a link starts. For a snap with your own timing, scrollSnap from svg-scroll-draw/snap glides to each section over the duration (600 ms by default) and with the easing you give it. The scrollSnap() mode of the generator shows it.
How do I know which item is snapped in JavaScript?
In Chromium browsers (from version 129) the container fires scrollsnapchange when the snapped item changes, with event.snapTargetInline and event.snapTargetBlock. Elsewhere, wait for scrollend and find the item whose snap position equals the scroll offset, as the preview’s readout does. With scrollSnap, onSnap(index) fires after every snap.
How do I add previous and next buttons to a scroll snap carousel?
Work out each item’s snap position (its edge minus the snap line, clamped to the scroll range) and scrollTo() the first one past the current offset, with behavior: 'smooth', or 'auto' for readers who ask for reduced motion. The generator’s snap.js does exactly that. Scrolling by one card’s width is shorter, but in our tests WebKit, Safari’s engine, did not snap a smooth scrollBy() and stopped it off by the scroll-padding.
Pick a curve for scrollSnap’s glide in the easing visualizer, or show how far down the page a reader is with the scroll progress bar generator. All of svg-scroll-draw 2.23.3: the docs.