Scroll✨ Premium

Horizontal Scroll

Six 180 × 140 px tiles on a 1,172 px track, inside an overflow: auto hidden container with scroll-snap-type: x mandatory: every gesture ends on a centred tile with its neighbours peeking from both edges — 1.4 KB of CSS, zero JavaScript, no animation: the finger or the wheel sets the pace. A .sb-horiz root carries the dark background and anchors the hint; the container is capped at 900 px so the track always overflows, even on a large screen.

CSSHorizontalSnap

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

Destination swatch — a travel agency's mobile app — Horizontal Scroll example 1

① Destination swatch — a travel agency's mobile app

WhenHome screen of a travel app: a title, a filter, and a row of destinations to swipe through, each tile showing the city, the country and a lead-in price.
WhyNeighbouring destinations stay partly visible on both sides: Paris to the left, Bangkok to the right, Tokyo in front. That peeking invites comparison at a glance and signals there are more. Horizontal Snap (fx-0594) gives each slide 100 % of the width and hides everything but the active destination; here the finger sets the pace and the snap centres each city.
SettingsThe .sb-horiz root is the 291 px area under the title (the scene keeps its own background), container at height: 100%; 220 × 230 px tiles, 18 px radius, text at the bottom over a linear-gradient(180deg, transparent 45%, rgba(0,0,0,.62)) veil; track padding: 8px 16px 26px, gap: 12px. At 540 px wide, 148 px of each neighbour stays visible; measured stops 0, 88, 320, 552, 784, 872.
Chronological imaging strip — a radiology app on a tablet — Horizontal Scroll example 2

② Chronological imaging strip — a radiology app on a tablet

WhenA patient's record on a tablet: an identification bar (patient, sequence), then the series of exams sorted by date, each image with its date and purpose.
WhyAdjacent images act as time markers: with October's follow-up centred, the practitioner still sees the edge of the previous and next exams. Widths may differ from one acquisition to the next (170 to 260 px here): scroll-snap-align: center centres each one whatever its size, where Card Snap Carousel (fx-0595) imposes 200 px cards with an image-title-text structure.
SettingsFive figure elements 190, 240, 170, 260 and 200 px wide by 240 px tall, 8 px radius, 1 px rgba(148,163,184,.22) border, caption on a #0e1628 background; track padding: 26px 16px 22px under a 2024 → 2026 axis; monospace font; container 540/1,140 px, 600 px of travel.
Finish picker — a touch kiosk in a kitchen showroom — Horizontal Scroll example 3

③ Finish picker — a touch kiosk in a kitchen showroom

WhenThe “Fronts” step of a configurator on a kiosk: light interface, a reminder of the current selection, and a swatch of finishes to swipe (oak, polished concrete, gloss, matte black, walnut, terrazzo) with item code and surcharge.
WhyScrolling is free: the customer compares two finishes side by side for as long as they like and reads the codes. Sticky Horizontal (fx-0578) would scroll on its own, on a 12 s animation, with no time to read; here the neighbouring finish peeks from the edge and signals the rest of the swatch, and the snap always aligns one finish at the centre of the kiosk.
SettingsLight theme: #f3f1ec scene, white 186 × 236 px tiles, #e2ddd3 border, 0 6px 18px rgba(60,50,30,.10) shadow, 160 px sample drawn with CSS gradients (wood as repeating-linear-gradient), current finish in outline: 3px solid; the “Scroll →” hint recoloured rgba(28,27,25,.55) to stay visible on a light background.

How it works

A root, three levels and a label. .sb-horiz is the effect's root: position: relative (the hint's anchor), width: 100%, #0a0a0f background, white text, sans-serif font — pasted into a white page, it brings its own background instead of relying on the page's. Inside it, .sb-horiz-content is the scroll container: width: 100%, max-width: 900px, margin: 0 auto, height: 280px, overflow: auto hidden (horizontal scrolling, nothing vertical), scroll-snap-type: x mandatory, display: flex and align-items: center. It holds .sb-horiz-track, a flex track with gap: 12px and padding: 20px 16px, lining up six .sb-horiz-item of 180 × 140 px (flex-shrink: 0, 14 px radius, bold white digit at 1.2 rem, scroll-snap-align: center). Each carries its own 135° gradient in a style attribute, and the chain closes on itself: one tile's end colour is the next tile's start (#6366f1 → #8b5cf6 → #d946ef → #f43f5e → #f59e0b → #22c55e → #06b6d4). Track: 6 × 180 + 5 × 12 + 2 × 16 = 1,172 px. Underneath, .sdc-scroll-hint “Scrollez →”: absolute inside the root, 8 px from the bottom, 0.65 rem, white at 30 %, its opacity swinging from .3 to .7 every 2 s (sdcHintPulse). The JavaScript field is empty: the whole mechanism is the browser's.

Snapping. At the end of each gesture the browser settles the scroll on a snap position: a tile centred in the container (center). Positions outside the scroll range are clamped to the edges. Measured in Chromium with a 728 px container (744 px page): 444 px of travel and only four stops for six tiles — 0, 126, 318, 444; only tiles 3 and 4 ever centre, tiles 1-2 and 5-6 are only seen at the ends, in pairs. At 374 px (mobile): 798 px of travel, stops at 0, 111, 303, 495, 687, 798, tiles 2 to 5 centre. At 900 px, the container's cap on a desktop screen: 272 px of travel, stops at 0, 40, 232, 272 (scrollLeft = 100 reads back 40). One horizontal wheel notch (100 units) moves 117 px then settles at 126. Chromium also snaps programmatic writes: scrollLeft = 100 reads back 126, 50 reads 0 — the carousel cannot be left “in between”. By touch, each 150 px swipe lands on the next tile (111 → 303 → 495 → 687) and a fast 300 px flick runs to the end (798): mandatory dictates the stop, not the speed. No scroll-behavior: a scrollTo() is instant unless you pass behavior: 'smooth' (≈ 230 ms for 192 px in Chromium).

Why a root and a cap. The hint is position: absolute: it anchors to the nearest positioned ancestor, and without .sb-horiz that would be the window (measured on a version without the root: hint at y = 579 in a 600 px window while the carousel ended at y = 408). The root keeps it 8 px from the bottom of the carousel wherever the code is pasted, and it carries the dark background on which its 30 % white stays visible. The cap comes down to one property of scrolling: a container only scrolls if its content is longer than it is. Without max-width, a width: 100% container is 1,264 px wide on a 1,280 px page: the 1,172 px track fits entirely, scrollWidth = clientWidth, zero travel, nothing scrolls (measured). Capped at 900 px, travel is 272 px on that same page; in a column that is already narrower (the catalogue preview's 744 px, a phone's 374 px), the cap plays no part. Finally, the vertical wheel does nothing (0 px after three notches of 120): a horizontal container only listens to its own axis, and that is deliberate — hijacking the vertical wheel would trap the page's scrolling as it passes the carousel. With a mouse you need Shift + wheel, a side wheel, the scrollbar or the keyboard; trackpads and fingers scroll natively.

Accessibility

  • prefers-reduced-motion is absent from the code. The only animation is the hint's opacity pulse (.3 ↔ .7, 2 s, infinite); measured under reduce, sdcHintPulse keeps running. Snapping itself is browser scrolling, not an animation, and without scroll-behavior: smooth there is nothing else to neutralise. Add @media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none } }.
  • Keyboard. Measured in Chromium: Tab focuses the container (scroll areas with no focusable child are focusable since Chrome 130, and have long been in Firefox), Right Arrow advances one stop (126, then 318). Safari does not: set tabindex="0", role="region" and an aria-label (“Destinations”, “Exams”) on .sb-horiz-content. If the tiles become links, drop the tabindex: Tab will scroll the focused tile into view.
  • Contrast. The “Scrollez →” hint is 10.4 px of white at 30 % under a pulsing opacity: 1.22:1 to 1.83:1 on #0a0a0f (2.62:1 without the pulse) — never the required 4.5:1, and the text is hard-coded in French. The root imposes that dark background: the ratio does not depend on the host page. Either turn it into a real, legible instruction, or mark it aria-hidden="true" and put the instruction in the container's aria-label. The tiles' white digits: 4.47:1 on #6366f1 but 2.15:1 on #f59e0b, 2.28:1 on #22c55e, 2.43:1 on #06b6d4 — placeholders to replace; if text stays on top of an image, add a dark overlay.
  • Screen readers and affordance. Six div with no list semantics: switch to ul/li (or role="list") so the item count is announced; off-screen tiles stay in the accessibility tree, which is correct. overflow: auto leaves the native, unstyled scrollbar: on systems with classic scrollbars (Windows, macOS set to “always show”) it sits at the bottom of the container's 280 px — it is the only affordance mouse users get, so do not hide it (scrollbar-width: none) without adding buttons.

Browser compatibility

Requires scroll-snap-type / scroll-snap-align (2018 specification), the two-value overflow: auto hidden syntax and gap in a flex context — the last one sets the threshold. No JavaScript, no CSS variables, no animation-timeline: unlike this category's native CSS scroll-driven effects, this one works in Firefox.

Chrome 84+✓ Full
Firefox 68+✓ Full
Safari 14.1+✓ Full
Edge 84+✓ Full
Mobile iOS 14.5+✓ Full (finger swipe, stops on each tile)
Android Chrome 84+✓ Full (finger swipe, stops on each tile)

Without JavaScript: nothing to lose, there is none. Safari 13.1 to 14.0 ignores flex gap: the tiles touch, scrolling and snapping still work. Safari 11 to 13.0 rejects overflow: auto hidden: the container is no longer one, the 1,172 px track overflows and nothing scrolls — write overflow-x: auto; overflow-y: hidden. Without snapping (Firefox before 68): free horizontal scrolling with no stop on the tiles.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="sb-horiz">
  <div class="sb-horiz-content">
    <div class="sb-horiz-track">
      <div class="sb-horiz-item">1</div>
      <div class="sb-horiz-item">2</div>
      <div class="sb-horiz-item">3</div>
      <div class="sb-horiz-item">4</div>
      <div class="sb-horiz-item">5</div>
      <div class="sb-horiz-item">6</div>
    </div>
  </div>
  <span class="sdc-scroll-hint">Scrollez →</span>
</div>
🔒 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
Container cap (CSS — .sb-horiz-content) width: 100%; max-width: 900px; margin: 0 auto The rule: it scrolls as long as the track (1,172 px for six tiles) is longer than the container. Measured on a 1,280 px page: 272 px of travel with the cap, zero without. Fit it to your column, or drop it if the column is already narrower than the track; widen the track (more tiles, or wider ones) before widening the container.
Background, colour and font (CSS — .sb-horiz) #0a0a0f, #fff, sans-serif The root imposes its background: that is what keeps the hint visible on a white page. For a carousel on a light background, change the background here and recolour the hint (rgba(28,27,25,.55) in the showroom scene); the font only affects the digits and the hint.
Tile size (CSS — .sb-horiz-item) 180 × 140 px Each tile may have its own width (class or style): scroll-snap-align: center centres any width. The narrower the tile relative to the container, the more the neighbours peek.
scroll-snap-align (CSS — .sb-horiz-item) center start aligns the tile's left edge with the container's: the first tile becomes reachable at 0 and the last one docks to the right. Then add scroll-padding-inline-start: 16px to the container to respect the track's inset.
scroll-snap-type (CSS — .sb-horiz-content) x mandatory x proximity only settles on a tile when you stop near one, and allows in-between positions; none makes scrolling free. With mandatory, even a scrollLeft written from JavaScript is clamped to a stop (100 → 126 measured).
Track spacing and inset (CSS — .sb-horiz-track) gap: 12px; padding: 20px 16px The gap is the visible margin between a centred tile and its neighbours; the padding sets where the ends are. For the first and last tiles to centre, raise the horizontal padding to half the container minus half a tile (274 px at 728 px wide).
“Scrollez →” hint (HTML/CSS — .sdc-scroll-hint) .65rem, rgba(255,255,255,.3), 2 s pulse Anchored to the .sb-horiz root (position: relative): keep it outside the scroll container, otherwise it leaves with the content. Contrast 1.22 to 1.83:1: replace it with a legible instruction, or remove it along with its rule and the sdcHintPulse animation.
Tile backgrounds (HTML — style attributes) 6 chained 135° gradients (#6366f1 → … → #06b6d4) Replace with background: url(…) center/cover or an img in object-fit: cover inside the tile; the gradients are inline, they go away with the HTML.

FAQ

Because a container only scrolls when its content overflows. The track is 1,172 px for six tiles: on a 1,280 px page, an uncapped width: 100% container is 1,264 px wide, all six tiles fit on one line and scrollWidth = clientWidth — zero travel, measured. At 900 px, travel is 272 px and snapping gets its stops back (0, 40, 232, 272). You can raise the cap, or drop it, as long as your track stays longer than your column: with your own tiles (ten 240 px photos, say), the track exceeds 2,500 px and the cap becomes pointless. The catalogue preview is 744 px wide: it scrolls with or without it.
Same container (overflow: auto hidden, scroll-snap-type: x mandatory) for the first two, but not the same content. Horizontal Snap: slides at min-width: 100% aligned start, one visible at a time, no glimpse of the neighbours. Card Snap Carousel: 200 × 240 px cards with a fixed structure (100 px image, title, text, tag), gap and padding on the container itself, and scroll-behavior: smooth. Sticky Horizontal does not scroll at all: its track slides on a 12 s CSS animation (translateX from 0 to −400 %) inside an overflow: hidden. Horizontal Scroll is the bare version: 1.4 KB of CSS, 180 × 140 px tiles with no inner structure, free width per tile, no scroll-behavior — pick it when the user must keep control of the pace and see what is coming.
Measured: three notches of the vertical wheel leave scrollLeft at 0 — a horizontal container only listens to the horizontal axis (Shift + wheel, side wheel, trackpad, scrollbar, or Tab then Right Arrow in Chrome 130+ and Firefox). For buttons, a scrollBy({ left: 192, behavior: 'smooth' }) is enough (width + gap): with mandatory, Chromium clamps the position to the nearest stop, so even a rough offset lands on a tile (100 → 126 measured). To disable the arrow at the end, compare scrollLeft with scrollWidth − clientWidth in a scroll listener, or scrollend if you do not need Safari before 26.