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
startstart edge123456snap linescroll portcentermiddle123456endend edge123456scroll-paddingsnapportscroll-paddingsnappositionstop row is here1234–60max
Fig. 1 — the snap line is the start, centre or end of the snapport: the scroll port minus scroll-padding. A snap position is the scroll offset that puts an item’s edge on it, clamped to what the box can scroll, so item 1 rests at 0 and items 4–6 share the end.
01The tool

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.

layout
snaps with
axis
scroll-snap-type
scroll-snap-align
scroll-snap-stop
scroll-padding · 16px
item size · 80%
gap · 16px
items · 8

preview · the generated CSS, as it isswipe, scroll, or focus it and use the arrow keys

1
2
3
4
5
6
7
8
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.

index.html13 lines
<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.css12 lines
.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;
}
snap.js28 lines
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));
02The model

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.

carousel.css12 lines
.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;
}
Clamped
A position below 0 or past the end cannot be reached, so it is moved to the end it overshoots. That is why the first card cannot be centred, and why several items near the end can share the last position: the rulers in the generator show it.
The whole page
To snap the page itself, put the snap type on html, not body, and the alignment on sections as tall as the window.
page.css8 lines
html {
  scroll-snap-type: y mandatory;
}

section {
  height: 100vh;
  scroll-snap-align: start;
}
035 properties

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.

PropertyGoes onValuesWhat it does
scroll-snap-typethe scroll containernone, or an axis (x, y, block, inline, both) then mandatory or proximityTurns snapping on for that axis. With no second word it is proximity.
scroll-paddingthe scroll containera 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-aligneach childnone, start, center, end: one value for both axes, or block then inlineWhich edge of the child lands on the snap line. none, the default, never snaps.
scroll-snap-stopeach childnormal, alwaysWith always, a scroll that would pass this child stops on it, so a fast swipe cannot skip it.
scroll-margineach childa length per sideGrows the child’s snap area by that much, so it lands that far from the line without the child moving.
04Honestly

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.

AspectCSS scroll snapscrollSnap()
NeedsNothing 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 motionThe 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 snapsWhen 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 upThe 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 sizesAny: cards narrower than the box, mixed sizes.Sections that fill the box. Smaller ones near the end can never reach its start.
Axesx, y or both.One: direction 'vertical' (the default) or 'horizontal'.
Knowing where it isscrollsnapchange in Chromium browsers (version 129 on); elsewhere scrollend and your own sums.onSnap(index) after every snap, and getCurrentIndex().
KeyboardThe 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 motionUp 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().

OptionDefaultWhat it does
direction'vertical''vertical' or 'horizontal': the axis it watches and glides along.
duration600How 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'.
threshold0.3How 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.
scrollContainerthe windowA selector or an element to snap inside, instead of the page.
onSnapnoneCalled with the section’s index after each snap.
respectReducedMotiontrueFor readers who ask their system for less motion, jump to the section instead of gliding. It still snaps.
main.js15 lines
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.
Not both
Do not give the container 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.
in 2.23.3
scrollSnap counts from the first section when it is made, wherever the page is, so make it with the page or box at the top: made further down, its first snap goes to the second section. And a border on the scroll container moves every landing by the border’s width, so put the border on a wrapper.
057 questions

Scroll snap questions

Q1

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.

Q2

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.

Q3

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.

Q4

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.

Q5

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.

Q6

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.

Q7

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.