Horizontal Scroll
Six 200 × 200 px tiles — a 32 px number and a short label — in a flex track with a 12 px gap that scroll-snap-type: x mandatory stops with a tile centered, a 212 px pitch, in CSS alone (zero bytes of JavaScript). Measured on 524 px of width: one horizontal wheel notch or a 150 px swipe advances one tile; the first and last tiles never center (±146 px); beyond 1,292 px of container nothing scrolls anymore while the “Scroll →” hint keeps pulsing.
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
Three levels and not one line of script. .ss-horiz-content is the viewport: height: 280px, overflow: auto hidden (horizontal scrolling, vertical cut off), display: flex with align-items: center, and above all scroll-snap-type: x mandatory. Inside, .ss-horiz-track lines up six .ss-horiz-item in flex with gap: 12px and padding: 20px 16px. Each tile: flex-shrink: 0, 200 × 200 px, border-radius: 14px, centered column, label at .9rem (14.4 px) bold, a span for the number at 2rem (32 px), weight 800, opacity: .5, and scroll-snap-align: center. Six 135° gradients via :nth-child (indigo → violet → fuchsia → rose → amber → green → cyan). The track measures 6 × 200 + 5 × 12 + 32 = 1,292 px. No CSS variable, empty js: the scrolling is the browser's own, on the compositor, with no listener.
Measured in Chromium on a 524 px viewport: scrollWidth 1,292, maximum travel 768 px. The rest positions are the tile centers minus half the viewport: 66, 278, 490, 702 — a 212 px pitch (200 + 12). Tile 1 would need −146 px and tile 6 +914: the bounds clamp them to 0 and 768, so neither the first nor the last tile ever centers; they stay 146 px off. mandatory forbids any in-between position: a scrollLeft forced to 50 settles at 66, 320 → 278, 900 → 768. One horizontal wheel notch (deltaX 120) advances exactly one tile (66 → 278 → 490 → 702 → 768); the vertical wheel does nothing (0 px); without JavaScript, dragging with the mouse does nothing either — trackpad, touch or the scrollbar only. On an emulated iPhone 13 (374 px), a 150 px swipe gives 141, 353, 565, 777 (same 212 px pitch), a 40 px swipe goes back to 0. No scroll-behavior: smooth: writing scrollLeft jumps; scrollTo({ behavior: 'smooth' }) glides (0 → 490 px in ~320 ms) and the snap finishes the move.
What the code leaves unsettled. The viewport has no width of its own: as a flex child of the sold .demo-preview (centered), it shrinks to the parent's width because a scroll container has min-width: 0; in a block parent it takes 100%. As soon as the parent exceeds 1,292 px, the whole track fits, centered, and nothing scrolls anymore (measured at 1,400 px: travel 0); at 1,264 px a 28 px stub remains between two snap positions. In both cases .sdc-scroll-hint — “Scrollez →”, an sdc- prefix shared with the catalog's other scroll demos, an opacity pulse .3 ↔ .7 over 2 s — keeps blinking. Nothing marks the centered tile (no active state), and the sold HTML mixes English labels (Design, Develop, Test…) with a French hint. The catalog's “Sticky” tag matches nothing in the code: the sticky, self-animated variant is Sticky Horizontal (fx-0578).
Accessibility
- prefers-reduced-motion is absent from the code — and, for once, almost without consequence: scrolling is only ever triggered by the user (no scroll-linked animation, no automatic motion). Only the hint's pulse ignores it: measured under
reducedMotion: reduce, its opacity still swings from 0.34 to 0.70 within one second. Add@media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none; opacity: .5 } }, or drop the hint when integrating. - Keyboard:
.ss-horiz-contenthas notabindex. Chrome 130+ makes any scroll container without focusable children focusable — measured: Tab stops on it and → advances one tile (scrollLeft 0 → 66) —, Firefox and Safari do not. Settabindex="0"and anaria-labelon the viewport. If the tiles act as a selector (families, chapters), they aredivs: replace them withbuttons or links and managearia-currenton the one driving the content. - Insufficient contrast on five tiles out of six (white 14.4 px bold label, 4.5:1 threshold): tile 1 indigo 4.4:1, tiles 2 and 3 between 3.5 and 3.9:1, tiles 4 to 6 (rose → amber → green → cyan) between 2.2 and 2.9:1. The number at
opacity: .5falls between 1.5 and 2.2:1. Fix it with darker gradient stops (#b45309,#15803d,#0e7490in place of the amber, green and cyan), atext-shadow: 0 1px 2px rgba(0,0,0,.35), or a label of at least 18.7 px bold (large text, 3:1 threshold). - Screen readers and integration: six
divs read as “01 Design”, “02 Develop”… in order, with no role and no announcement of the centered tile — the scroll position carries no semantics. Touch works natively (one tile per swipe, a short swipe settles back). For the first and last tiles to center like the others, give the trackpadding: 20px calc(50% - 100px)(which is what the three scenes do); to avoid misleading users on wide screens, cap the viewport (max-width) or hide the hint whenscrollWidthdoes not exceedclientWidth.
Browser compatibility
Stylesheet only, no JavaScript: scroll-snap-type: x mandatory and scroll-snap-align: center (2018 syntax), two-value overflow (auto hidden), flexbox gap, linear gradients. Scrolling is native: the effect works in Firefox, unlike CSS scroll-driven animations (animation-timeline), which Firefox does not enable in any version.
Without scroll-snap (older browsers), the track scrolls freely and stops anywhere, but everything stays visible and readable. Without flexbox gap (Safari 13 and earlier), the tiles touch and the pitch drops from 212 to 200 px. Nothing depends on a script: there is none.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="ss-horiz-content">
<div class="ss-horiz-track">
<div class="ss-horiz-item">
<span>01</span> Design
</div>
<div class="ss-horiz-item">
<span>02</span> Develop
</div>
<div class="ss-horiz-item">
<span>03</span> Test
</div>
<div class="ss-horiz-item">
<span>04</span> Deploy
</div>
<div class="ss-horiz-item">
<span>05</span> Monitor
</div>
<div class="ss-horiz-item">
<span>06</span> Iterate
</div>
</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 |
|---|---|---|
Tile size (CSS — .ss-horiz-item width / height) |
200 × 200 px | The snap pitch is width + gap, i.e. 212 px: the distance of one wheel notch or one swipe. Visible tiles = viewport width ÷ 212 (two and a half at 524 px). The scenes go down to 128 × 92 px without changing anything else. |
Gap (CSS — .ss-horiz-track gap) |
12px | Adds to the snap pitch. At 0 the tiles touch and the pitch becomes 200 px; at 24 the gradients breathe but one fewer tile fits on screen. |
Track gutters (CSS — .ss-horiz-track padding) |
20px 16px | With 16 px, the first and last tiles stop 146 px from the center. padding: 20px calc(50% - 100px) (half the viewport minus half a tile) centers them like the others — the setting used by all three scenes. |
Snap strictness (CSS — .ss-horiz-content scroll-snap-type) |
x mandatory | x proximity only realigns near a tile and allows stopping in between — preferable if tiles are wider than the viewport (mandatory would make the overflow unreachable). none: free scrolling, the effect disappears. |
Alignment point (CSS — .ss-horiz-item scroll-snap-align) |
center | start aligns the tile on the left edge: the first one then naturally rests at 0, and the left padding stays visible through scroll-padding-left: 16px on the viewport. |
Number of tiles and colors (HTML + CSS — :nth-child(1) to (6)) |
6 gradients at 135° | A seventh tile has no background: add an :nth-child(7) rule or move the gradients to inline style as fx-0541 does. The track grows by 212 px per tile. |
Viewport height (CSS — .ss-horiz-content height) |
280px | The track (200 + 2 × 20 = 240 px) is centered in it. Reduce it along with the tiles: 118 px for 92 px tiles in the catalog scene. |
Programmatic scrolling (CSS — scroll-behavior, absent) |
auto (instant jump) | For a “next” button or a scrollLeft = … to glide, add scroll-behavior: smooth on the viewport, or call scrollTo({ left, behavior: 'smooth' }) — measured: 0 → 490 px in ~320 ms, the snap finishes the move. |
FAQ
scroll-snap-align: center asks for a negative scroll position for the first (center at 116 px, half viewport 262 px: −146) and one beyond the travel for the last (+914 px for 768 of travel). The browser clamps those positions to the 0 and 768 bounds: both tiles stop 146 px from the center, measured on 524 px. Two fixes: track gutters equal to half the viewport minus half a tile (padding: 20px calc(50% - 100px)), or start alignment with scroll-padding-left. Without it, an “active tile = closest to center” script will never select the first or the last.max-width on .ss-horiz-content (720 px = three centered tiles), or hide the hint when scrollWidth ≤ clientWidth — one line of JavaScript, the only one the effect would ask for.style gradients: thumbnails, not labels. fx-0586 adds a short label under a half-transparent number in a square 200 px tile: it is the “named steps or families” version. The lot's other neighbors answer other needs — Horizontal Snap (fx-0594): full-width panels one at a time; Card Snap Carousel (fx-0595): 200 × 240 px cards with an image area, text and a tag, plus scroll-behavior: smooth; Sticky Horizontal (fx-0578): automatic scrolling over 12 s, with no user control.