Horizontal Snap
Five sections at min-width: 100% inside a 280 px container with overflow: auto hidden and scroll-snap-type: x mandatory: each slide fills the full width and hides its neighbours, without a line of JavaScript. Measured: how far one wheel notch travels depends on the browser — one slide in Firefox and Safari, back to zero in Chrome until the gesture reaches half the width.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →
3 usage examples



How it works
Two classes do everything. .sn-hsnap-container: width: 100%, height: 280px, overflow: auto hidden (horizontal scrolling, vertical cut off), scroll-snap-type: x mandatory, scroll-behavior: smooth, display: flex. Inside, five .sn-hsnap-section: min-width: 100% and flex-shrink: 0 — each section is exactly as wide as the container, never less —, height: 280px, scroll-snap-align: start, a centred flex column (gap: 8px, padding: 20px), an h4 at 0.9 rem (14.4 px) and a p at 0.72 rem (11.5 px) in 60% white, and a 135° gradient per :nth-child(1) to (5), from #0f172a → #1e293b to #64748b → #94a3b8. Measured in a 1,280 px zone: five 1,280 px sections, scrollWidth 6,400 px, no console error, empty js — the effect is pure CSS. The rest of the sold HTML is catalogue furniture: the .demo-preview wrapper (centred flex, min-height: 300px), the .sdc-scroll-hint hint “Scrollez →” (10.4 px at 30% white, a 2 s looping sdcHintPulse, absolutely positioned 8 px from the bottom of the wrapper, hence straddling the container's bottom edge) and five inline style="font-family: Inter…" on the headings.
The mechanism is native scroll snap: mandatory forces the browser, at the end of every gesture, to settle on a snap point — the start edge of a section, i.e. a multiple of the width. scroll-behavior: smooth only concerns programmatic scrolls (scrollTo, anchors); wheel, keyboard and finger are smoothed by the browser, not by the code. And that is where the engines diverge, measured on a 1,280 px slide. Chrome 153 (headed, real smoothing): a 120 px wheel notch moves to 118 px then returns to 0 in 170 ms; 600 px return as well; 700 px reach slide 2; three notches 60 ms apart fall back to 0 — the threshold is half the width (640 px), the snap point nearest the end of the gesture. A synthesised 220 px touch swipe returns to 0, 700 px advance one slide. Firefox: any wheel event is worth exactly one slide (30, 120 or 3,000 px → 1,280), three notches → three slides. WebKit: one notch → one slide with an ease measured at 1 s; 3,000 px at once → three slides. In all three, a vertical-only wheel gives 0: a plain mouse never moves the slides without Shift (a conversion done by the browser, not the code).
Three constraints come from the dimensions. The two fixed heights (280 px) and overflow-y: hidden: a slide whose content exceeds 240 px (280 − 2 × 20 padding) is clipped top and bottom, with no vertical scrolling possible — measured with 30 lines: the paragraph spans from −42 px to +348 px in a 280 px section, lines 1 to 3 and 25 to 30 are invisible. The classic scrollbar (Windows, Linux, macOS set to “always”): 15 px taken inside the container's 280 px (clientHeight 265) while the sections keep 280 — the bottom of every slide goes under the bar. End of travel: on slide 5 (scrollLeft 5,120), one more notch does nothing, no wrap to the start. Finally the keyboard: Chrome 153 puts the container in the tab order (keyboard-focusable scrollers) and → advances one slide per press in 830 ms; Firefox, WebKit and headless Chromium tab straight past it — a tabindex="0" settles it; Home and End act on the vertical axis and do nothing here.
Accessibility
- prefers-reduced-motion not in the code. Measured under an emulated preference (Chrome 153):
scroll-behaviorstayssmooth, the keyboard snap still takes 830 ms, the hint's pulse keeps running (1 active animation). Add@media (prefers-reduced-motion: reduce) { .sn-hsnap-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }— the wheel and finger easing, for its part, belongs to the browser and follows the system setting, not your CSS. - Keyboard: the container is in the tab order only in recent Chrome (measured: Tab stops on it in Chrome 153, skips it in Firefox, WebKit and headless Chromium). Set
tabindex="0",role="region"and anaria-label; ← → then move one slide per press (measured 1,280 → 2,560 → 3,840). Home and End do nothing on the horizontal axis, and a vertical-wheel mouse does nothing at all: provide visible previous/next buttons. - Contrast measured on pixels (Chrome, centre of the slides): slides 1 and 2 pass (white heading ≥ 14:1, 60% text ≥ 4.9:1); slide 3, text 4.46:1 (just under 4.5); slide 4, text 3.39:1; slide 5, heading 3.56:1 and text 2.27:1. The two lightest gradients fail for 14.4 px bold and 11.5 px text. Move the paragraph to
rgba(255,255,255,.85)or stop the gradients at#475569 → #64748b. The “Scrollez →” hint: 10.4 px at 2.6:1 — furniture to remove. - Screen readers: the five sections are native content, nothing is hidden from speech — all five headings are read in a row whatever slide is shown, including what
overflow-y: hiddenclips visually. Give the containerrole="region"+aria-roledescription="carousel"and each sectionrole="group"+aria-label="Slide 2 of 5". No live region exists: without JavaScript, a slide change is never announced.
Browser compatibility
Pure CSS, no animation-timeline: two-value overflow (auto hidden), scroll-snap-type: x mandatory, scroll-snap-align, scroll-behavior: smooth, flex gap and linear-gradient. The scrolling is native: Firefox handles it, unlike CSS scroll-driven animations (animation-timeline), which it supports in no version. No dependencies.
Without the two-value overflow syntax (Chrome < 68, Firefox < 61, Safari < 13.1), the whole declaration is ignored: overflow: visible, no scroll container any more — the five sections overflow the page to the right and nothing snaps. Write overflow-x: auto; overflow-y: hidden as two properties to cover those versions. Without scroll-behavior (Safari < 15.4), programmatic scrolls are instant; without flex gap (Safari < 14.1), heading and text touch. Without JavaScript: nothing changes, there is none.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="sn-hsnap-container">
<div class="sn-hsnap-section">
<h4>Slide 1</h4>
<p>Drag horizontally</p>
</div>
<div class="sn-hsnap-section">
<h4>Slide 2</h4>
<p>Intuitive navigation</p>
</div>
<div class="sn-hsnap-section">
<h4>Slide 3</h4>
<p>Perfect for galleries</p>
</div>
<div class="sn-hsnap-section">
<h4>Slide 4</h4>
<p>Optimised mobile experience</p>
</div>
<div class="sn-hsnap-section">
<h4>Slide 5</h4>
<p>Last slide</p>
</div>
</div>
<span class="sdc-scroll-hint">Scroll →</span>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Number of slides (HTML — .sn-hsnap-section blocks) |
5 | Add or remove sections; the scroll width follows (measured 5 × width). Gradients are only defined for :nth-child(1) to (5): a sixth slide has no background. |
Height (CSS — height of .sn-hsnap-container and .sn-hsnap-section) |
280 px / 280 px | Keep both equal. 100dvh on both for full-screen slides. Content beyond height − 40 px is clipped (overflow-y: hidden), and a classic scrollbar takes 15 px out of that height. |
Slide width (CSS — min-width: 100%) |
100% | This is what hides the neighbours. At 85%, the edge of the next one appears and the start snap leaves it peeking on the right: you are then on fx-0595's territory, which adds gap and scroll-snap-align: center. |
scroll-snap-type (CSS — .sn-hsnap-container) |
x mandatory | x proximity lets the user stop between two slides if the gesture ends far from a snap point; mandatory forbids it. Chrome's threshold (half the width) does not change. |
scroll-behavior (CSS — .sn-hsnap-container) |
smooth | Only acts on scrollTo, scrollBy and anchors — your previous/next buttons. auto under prefers-reduced-motion. |
Gradients (CSS — :nth-child(1) to (5)) |
#0f172a → #94a3b8, 135° | One colour pair per slide. The last two are too light for the sold text (measured 3.56:1 and 2.27:1): stop the ramp at #64748b or lighten the paragraph. |
Texts (CSS — h4 0.9 rem, p 0.72 rem at 60% white) |
14.4 px / 11.5 px | Catalogue-thumbnail sizes. For a full-screen slide, clamp(1.4rem, 3vw, 2.4rem) for the heading and 1 rem for the text at rgba(255,255,255,.85). |
overflow (CSS — .sn-hsnap-container) |
auto hidden | Two-value shorthand (x then y). Write overflow-x: auto; overflow-y: hidden for Safari < 13.1; overflow-y: auto lets a too-tall slide scroll, at the cost of nested scrolling under the finger. |
FAQ
container.scrollBy({ left: ±container.clientWidth }), or a wheel listener that turns deltaY into one whole slide.overflow: auto hidden, scroll-snap-type: x mandatory, flex —; everything hinges on the children's width. Here min-width: 100% and scroll-snap-align: start: one slide fills the frame and no neighbour peeks out. fx-0595 lays 200 × 240 px cards with gap: 12px and scroll-snap-align: center, fx-0541 centred 180 × 140 px tiles: the neighbours show at the edge, which invites swiping but splits attention. A measurable consequence of the full frame: Chrome's threshold is half a slide, 640 px here against about a hundred pixels on a 200 px card — hence the buttons of the previous question. Choose this one when each screen must be read on its own: a plan, a photo, a step.height: 100dvh to the container and to the sections (both together: the sold section has its own 280 px height). Anything beyond height − 40 px is lost: the container is overflow-y: hidden and the section centres its content, so clipping happens at the top and the bottom — measured with 30 lines in a 280 px section: the text runs from −42 px to +348 px, lines 1 to 3 and 25 to 30 invisible, scrollHeight 348 with no way to scroll. For a plan with ten guarantees, trim the content or set the section to overflow-y: auto with height: 100% — knowing that vertical scrolling inside a horizontally snapping container gets in the way of finger swipes. On Windows, reserve 15 px at the bottom or hide the bar (scrollbar-width: none) while providing buttons.