Scroll✨ Premium

Card Snap Carousel

Six 200 × 240 px cards in a 280 px track set to scroll-snap-type: x mandatory, each one snapped to the center (scroll-snap-align: center): the neighbors peek from both edges as long as the track is narrower than 624 px. No JavaScript — and scroll-behavior: smooth only eases scripted scrolling, not the wheel.

CSSCarouselSnap

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

SaaS subscription plan comparator — five plans in a 540 px track — Card Snap Carousel example 1

① SaaS subscription plan comparator — five plans in a 540 px track

WhenPricing page of a project-management tool: Free, Starter, Pro, Team, Enterprise, each on a 200 px card with its price, its quota and one key mention.
WhyIn a 540 px track, the centered card leaves 158 px of each neighbor: the prospect reads a plan without losing sight of the previous and the next, and compares step by step. fx-0594 (Horizontal Snap, sections at min-width: 100%) hides every other plan while one is being read — the side-by-side comparison, the one that helps decide, is gone.
SettingsFive cards; the price at 1.5 rem in .sn-carousel-img instead of the number, gradients recolored in indigo; the recommended plan gets an accent border; opens on card 3 through a scrollLeft write, eased by scroll-behavior.
Independent music festival program — one stage and one slot per card — Card Snap Carousel example 2

② Independent music festival program — one stage and one slot per card

WhenThe festival's page or app: Stage A, Stage B, Outdoor, Club and the after-party; on each card the slot, the artist and the genre.
WhyThe visible edges say “there are other stages” without forcing anything: the festival-goer stops on a card, reads it, then moves on. fx-0578 (Sticky Horizontal) runs its five panels on its own, 2.2 s each over a 12 s cycle — no time to read a slot before the next one.
SettingsFive cards; .sn-carousel-img reduced to the time slot above the stage name; warm gradients; track left at 280 px under an 80 px header; “Scroll →” hint kept.
Consulting firm case-study catalogue — six references in horizontal scrolling — Card Snap Carousel example 3

③ Consulting firm case-study catalogue — six references in horizontal scrolling

WhenA firm's “References” page: client monogram, quantified result, sector, one tag per practice area.
WhyThe peeking neighbor cards convey the abundance of the catalogue better than a list. fx-0586 (Horizontal Scroll) also snaps its tiles to the center, but each is just a number and a word in a 200 px square: nothing to read, nothing to compare from one tile to the next. Here every card carries a result.
SettingsSix cards, the six original nth-child gradients, the monogram instead of the number, the key result in h4, the sector in p, the tag = practice area.

How it works

.sn-carousel-container is the track: width: 100%, height: 280px, overflow: auto hidden (scrolls in x, never in y), scroll-snap-type: x mandatory, scroll-behavior: smooth, flex with align-items: center, gap: 12px and padding: 0 12px. Inside, six .sn-carousel-card of 200 × 240 px (flex-shrink: 0, border-radius: 14px, 4% white background, 1 px border at 8%), each carrying scroll-snap-align: center. A card = a 100 px .sn-carousel-img band (number 01 to 06 at 1.5 rem / 800, a 135° gradient per nth-child: indigo → violet → fuchsia → rose → amber → green → cyan) and a .sn-carousel-body (12 px padding, flex column, 4 px gap: h4 .82 rem, p .68 rem at 50% white, .sn-carousel-tag .65 rem violet pushed to the bottom by margin-top: auto). Total track: 12 + 6 × 200 + 5 × 12 + 12 = 1,284 px. The shipped HTML ends with .sdc-scroll-hint “Scrollez →”, absolutely positioned 8 px from the bottom of .demo-preview, its opacity pulsing from .3 to .7 every 2 s (sdcHintPulse) — a catalogue fragment shared by 44 effects of the category, not a part of the carousel. Zero JavaScript.

The snap arithmetic: the center of card n sits at 112 + 212 × (n − 1) px from the start of the track; the resting position is that center minus half the visible width, clamped between 0 and 1,284 − width. Measured in Chromium: a 524 px track → six stops (0, 62, 274, 486, 698, 760), cards 2 to 5 centered with 150 px of each neighbor visible, cards 1 and 6 stuck to the edge; a 384 px track (mobile) → 80 px of neighbor; a 728 px track → only 40 px; a 784 px track → four stops for six cards (0, 144, 356, 500): cards 1-2 share position 0, cards 5-6 share the end of travel, none of the four is ever centered. The symmetrical “peek” only exists below 624 px of track — beyond that, (width − 200) / 2 − 12 exceeds 200 px and the neighbor is fully visible.

scroll-behavior: smooth only acts on scripted scrolling: scrollLeft = 274 measured 0 → 3 → 14 → 42 → 105 → 180 → 207 → 226 → 239 → 254 → 260 → 265 at 16 ms intervals, about 200 ms; with auto, the same write is instant (274 on the first sample). With the wheel, the readings are identical with or without the property: one horizontal notch of 120 px carries the track to 119, then the snap pulls it back to 62 in about 180 ms — that is the browser's easing, not the CSS. The vertical wheel does nothing (three notches, 0 px): without a trackpad, it is Shift + wheel or the scrollbar. A 600 px flick jumps from 0 to 500, the end of travel, without stopping at the cards in between: no scroll-snap-stop: always. On the keyboard in Chromium, Tab focuses the track and Right Arrow advances one stop (0 → 144). No console errors.

Accessibility

  • prefers-reduced-motion is absent from the code: measured identically under the preference — scripted scrolling still goes through eleven intermediate values (1, 13, 41, 102…) and the sdcHintPulse animation stays running. Add @media (prefers-reduced-motion: reduce) { .sn-carousel-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }. The snap itself is not an animation: it stays, and that is desirable.
  • Keyboard: the track has no tabindex. Chromium makes it focusable by default (measured: Tab → .sn-carousel-container, Right Arrow = one stop), Firefox too, Safari does not. Set tabindex="0", role="region" and an aria-label, then a :focus-visible style — the CSS has none. The cards are not interactive; if you put links in them, each off-screen card stays tabbable and the browser brings it into view, snap included.
  • Contrast on the card background (#141419 once the 4% white sits on #0a0a0f): white title 18.4:1, p at 50% white 5.3:1, #a78bfa tag 5.8:1 — but at 10.9 px and 10.4 px, too small for useful text: go to 12-13 px. White numbers on the gradients: 4.5:1 (card 1) down to 2.2:1 (card 5, amber → green) and 2.3-2.4:1 (card 6), below the 3:1 threshold even for 24 px text. The “Scrollez →” hint swings from 2.6:1 to 9.7:1 at 10.4 px, and it is written in French in the HTML.
  • Mouse without a trackpad: vertical wheel = 0 px (measured). The code has neither previous/next buttons nor a position indicator: add two buttons calling scrollBy({ left: ±212 }) — the scroll-behavior property will ease the move. Nothing tells a screen reader there are six cards: a <ul> or role="list" / role="listitem". The scrollbar is not hidden (no scrollbar-width or ::-webkit-scrollbar rule): on Windows it takes about 17 px of the 280 px track, below the cards. It is the component's only position cue — keep it or replace it with dots.

Browser compatibility

CSS only: scroll-snap-type: x mandatory / scroll-snap-align: center (2018 syntax), two-value overflow, scroll-behavior: smooth, flex gap and width: fit-content. No animation-timeline: this is native scrolling, it works in Firefox — unlike CSS scroll-driven animations, which Firefox does not render in any version. Zero dependencies, zero script.

Chrome 84+✓ Full
Firefox 94+✓ Full
Safari 15.4+✓ Full
Edge 84+✓ Full
Mobile iOS 15.4+✓ Full (finger swipe)
Android Chrome✓ Full (finger swipe)

Chrome 69-83: full snap and easing but flex gap ignored, the cards touch. Firefox 68-93: everything works except width: fit-content, the tag pill stretches across the card body. Safari 11-15.3: native snap, but scripted scrolling is instant (scroll-behavior ignored); wheel and finger are unchanged. Without scroll-snap at all: a free horizontally scrolling row, with no stop on the cards.

The code

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

index.html — structure
<div class="sn-carousel-container">
  <div class="sn-carousel-card">
    <div class="sn-carousel-img">01</div>
    <div class="sn-carousel-body">
      <h4>Title</h4>
      <p>Description</p>
      <span class="sn-carousel-tag">Tag</span>
    </div>
  </div>
  <!-- × 6 cards -->
</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
Card width (CSS — .sn-carousel-card width) 200px With gap: 12px, the pitch between two stops is width + 12, and the visible share of each neighbor is (track − width) / 2 − 12. At 260 px in a 540 px track: 128 px of neighbor instead of 158.
scroll-snap-align (CSS — .sn-carousel-card) center start snaps the active card to the left edge — add scroll-padding-left: 12px on the track to honor the padding — and makes the first card reachable, but the last one no longer snaps.
scroll-snap-type (CSS — .sn-carousel-container) x mandatory x proximity only snaps near a stop and leaves free scrolling in between. To forbid skipping cards (measured: a 600 px flick goes from 0 to the end of travel), add scroll-snap-stop: always on .sn-carousel-card.
gap + padding (CSS — .sn-carousel-container) 12px / 0 12px Keep them equal: the first and last card then get the same margin as the space between two cards. The padding also sets the resting position of card 1 (12 px from the edge).
Heights (CSS — .sn-carousel-container height / .sn-carousel-card height) 280px / 240px 20 px free above and below the cards; that is where the scrollbar sits when the system shows one. At 260 px of track, it eats into the cards.
scroll-behavior (CSS — .sn-carousel-container) smooth Only concerns scrollLeft, scrollTo, scrollBy and anchors (about 200 ms measured); wheel and finger do not depend on it. Switch to auto under prefers-reduced-motion.
Band gradients (CSS — .sn-carousel-card:nth-child(1…6) .sn-carousel-img) 6 gradients at 135°, indigo → cyan Six rules for six cards: a seventh card has no background. Write nth-child(6n+1)… to cycle, or a single color for every band.
“Scrollez →” hint (HTML — .sdc-scroll-hint) bottom: 8px, opacity .3 ↔ .7 over 2 s Positioned against the nearest positioned ancestor (.demo-preview in the shipped code). Translate it, replace it with buttons or remove it: it is decorative and its contrast drops to 2.6:1 at the low point of the pulse.

FAQ

Because the resting position is clamped by the scrollable range: centering card 1 would need a negative scrollLeft, centering card 6 would overshoot the end of travel. Measured in a 784 px track: cards 1 and 2 share stop 0, cards 5 and 6 share stop 500 — four positions for six cards. The fix is padding that creates the missing room: .sn-carousel-container { padding: 0 calc(50% - 100px) } (half the track minus half a card). Every card then becomes centerable, first and last included, and the number of stops equals the number of cards whatever the width.
Same sn- prefix, same track (overflow: auto hidden, x mandatory, smooth, 280 px), but fx-0594 uses sections at min-width: 100% snapped to start: one panel per stop, nothing of the neighbors, always as many stops as sections. fx-0595 fixes 200 px cards snapped to the center: the neighbors peek, and the number of stops depends on the width (six at 524 px, four at 784 px). Pick fx-0594 for a sequence read one message at a time, fx-0595 to browse and compare several short items.
A track that only scrolls in x only listens to horizontal moves: trackpad, touch gesture, Shift + wheel or the scrollbar. Measured: three vertical notches, 0 px. To convert the vertical wheel: track.addEventListener('wheel', e => { if (!e.deltaY) return; e.preventDefault(); track.scrollBy({ left: e.deltaY }); }, { passive: false })scroll-behavior: smooth eases the move and the mandatory snap ends on a card. The price: the page no longer scrolls vertically while the pointer is over the track, including at the end of travel; test scrollLeft against 0 and scrollWidth − clientWidth before canceling the event. Two scrollBy({ left: ±212 }) buttons remain the most readable solution.