Scroll✨ Premium

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.

CSSSnapHorizontal

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

Cloud plan comparison for large enterprises — one plan per screen — Horizontal Snap example 1

① Cloud plan comparison for large enterprises — one plan per screen

WhenProduct page of a cloud host: Starter, Pro, Business, Enterprise, each on a full-width slide with price, three guarantees and a button; arrows and position dots under the block.
WhyEach plan takes the whole space: min-width: 100% and scroll-snap-align: start mean no edge of the neighbouring plan is visible — Card Snap Carousel (fx-0595) would let the next plan peek out and start the comparison before the reading. The prospect reads one plan, then moves to the next with a gesture or a click.
SettingsFour sections, height raised to 270 px, gradients replaced by one hue per plan, scrollbar hidden (scrollbar-width: none) and replaced by arrows calling scrollTo({ left: i × clientWidth }) — necessary in Chrome, where one wheel notch is not enough —, tabindex="0" and aria-label on the container.
Guided tour of a boutique hotel's spaces — one space per swipe — Horizontal Snap example 2

② Guided tour of a boutique hotel's spaces — one space per swipe

WhenOn mobile, the suite, the restaurant and the spa: a full-frame photo, a title and two lines per slide, the swipe reveals the next space.
WhyImmersion relies on the whole frame: nothing of the next space peeks out, unlike Horizontal Scroll (fx-0541) whose 180 px tiles show at the edge. The swipe is native (no touch listener), the mandatory snap guarantees you never stop between two photos.
SettingsThree sections, height = the phone screen's (324 px), gradients replaced by layered-gradient “photos”, content aligned bottom-left (justify-content: flex-end), text at rgba(255,255,255,.85), position dots updated on scroll, auto-advance every 1.1 s cut under prefers-reduced-motion.
First-steps tutorial of a mobile app — five screens, one gesture — Horizontal Snap example 3

③ First-steps tutorial of a mobile app — five screens, one gesture

WhenApp onboarding: Create your account, Invite your team, Import your data, Set up alerts, Let's go! — five full-frame screens, a Next button and dots.
WhyThe horizontal swipe is the native gesture of mobile onboarding; Vertical Snap (fx-0593) would impose a downward scroll that reads as going back in an app. The Next button does the same job as the finger (scrollTo by one width) and the snap realigns the screen to the pixel.
SettingsFive sections on a light background (gradients removed, background: #fff, heading #0f172a, text #475569), height 236 px, scroll-snap-type: x mandatory kept, “Next” button wired to scrollTo({ left: i × clientWidth }), step list synchronised on scroll.

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-behavior stays smooth, 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 an aria-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: hidden clips visually. Give the container role="region" + aria-roledescription="carousel" and each section role="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.

Chrome 84+✓ Full (69–83: without the 8 px gap)
Firefox 99+✓ Full (68–98: snapping rated partial by MDN)
Safari 15.4+✓ Full (13.1–15.3: scrollTo without easing)
Edge 84+✓ Full
Mobile iOS 15.4+✓ Full (native swipe)
Android Chrome 84+✓ Full (native swipe)

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):

index.html — structure
<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>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
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

Because Chrome picks, at the end of the gesture, the snap point nearest the position reached: on a 1,280 px slide, a 120 px notch lands at 118 px, and 0 is nearer than 1,280 — back to the start, measured in 170 ms in headed Chrome 153. You must pass half the width in a single gesture: 700 px go through, 600 px come back, three notches 60 ms apart come back too. Firefox advances one slide per wheel event, WebKit as well (with an ease of about 1 s), whatever the delta. And in all three, a vertical-only wheel does nothing — you need Shift + wheel, a trackpad or a finger. The fix is not in the CSS: two buttons calling container.scrollBy({ left: ±container.clientWidth }), or a wheel listener that turns deltaY into one whole slide.
The container is the same in all three — 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.
Give 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.