ScrollFree

Vertical Snap

Four 280 px sections in a 280 px box with overflow-y: auto: scroll-snap-type: y mandatory ends every gesture on the top of a section, scroll-behavior: smooth eases programmatic jumps. Two rules, zero JavaScript, zero dependencies: a .sn-vsnap-wrap frame carries the background, the font and the “Scrollez ↓” hint, and box-sizing: border-box keeps every section at 280 px, padding included (measured: 840 px scroll range, one section = one window).

CSSSnapVertical

Section 1

Faites défiler pour passer à la suivante

Section 2

Le scroll s'arrête automatiquement

Section 3

Expérience fluide et contrôlée

Section 4

Dernière section

Scrollez ↓
Hover or click the scene to interact

This effect is free — the Scroll category contains 70 effects total, including 12 free. Effect.Labs has 811 vanilla effects. Explore the category →

Usage examples

Lumen — Product team
to me · today, 09:12
Three new things in Lumen 4.2
Scroll the block below: one feature per screen.
1/3 ↓

Automation

Rules that trigger your workflows on their own: a form received, a plan changed, a deadline reached.

2/3 ↓

Reporting

Weekly reports built from your own views, sent as PDF to whoever you choose.

3/3

Integrations

Slack, HubSpot, Notion and 40 other tools connect in two clicks, with no API key to copy.

See the update

① Slides embedded in an interactive email — three features that snap without a line of script

WhenHTML email from a software vendor announcing an update: sender header, subject, then a block of three slides (Automation, Reporting, Integrations) the reader scrolls through, and a button below.
WhyMail clients block JavaScript; the three other snaps in this batch (fx-0584, fx-0597, fx-0598) rely on a script for their dots, bar or entrance animations and do nothing there. Here two CSS rules are enough in clients that render embedded CSS (Apple Mail, Outlook Web), and where a client ignores them, the three slides stack and still read fine.
Settings172 px box, sections at height: 100% (the sold code's box-sizing: border-box kept), scroll-snap-stop: always (one gesture = one slide), a hard-coded “1/3 ↓” counter in each slide since there is no script, scrollbar-width: thin, light background and #4f46e5 accent; the “Scrollez ↓” hint replaced by that counter. The slides advance on their own until the first gesture — a demo device, not something an email can do.
Guide › Quick start

Quick start

Three steps, one command each. Scroll the block.

Step 1 of 3

Install

$npm install -g orbit-cli

Node 20 or later. The binary lands in your PATH.

Step 2 of 3

Configure

$orbit init --region eu-west-3

Writes orbit.config.json at the project root.

Step 3 of 3

Deploy

$orbit deploy --prod

Builds, uploads and switches the URL, in about a minute.

② Generated static documentation component — Install, Configure, Deploy on three screens

WhenTechnical documentation site built with Eleventy or Hugo: sidebar, article, and inside the article a “steps” component where each screen shows one command and its note.
WhyZero runtime: the component is a Nunjucks partial or a Hugo shortcode with two CSS rules, no bundler and no script to load — fx-0584 and fx-0598 require a script these generators won't bundle without configuration. The page stays indexable and readable without JS, which documentation must be.
Settings196 px box, sections at height: 100% aligned left (align-items: flex-start over the sold code's centering), scroll-snap-type: y mandatory and scroll-behavior: smooth kept, step number, command in ui-monospace, dark theme; scrollbar left visible to say the block scrolls.
Competition rules · 2026-27 season 2nd half · 63:12
Article 12

Fouls and misconduct

A direct free kick is awarded when a player commits a foul carelessly, recklessly or with excessive force. Recklessness is cautioned, excessive force is sent off.

Next article ↓
Article 13

Free kicks

The ball is in play once it is kicked and clearly moves. Opponents stay at least 9.15 m from the ball until it is in play.

Next article ↓
Article 14

The penalty kick

The kicker is identified, the goalkeeper stays on the line until the ball is kicked, every other player outside the area and at least 9.15 m from the mark.

Back to article 12 ↓

③ Sports rulebook article by article on a pitch-side tablet — one article per screen, the thumb does the rest

WhenA referee consults the competition rules on a tablet during the match: header with the clock, one article per screen, a thumb swipe up moves to the next.
WhyNot a byte of JavaScript to load or run: nothing breaks if the network drops or the Android tablet is five years old — fx-0597 (JS + progress bar) can fail silently if IntersectionObserver is missing. The snap guarantees a whole article is on screen, never two halves.
SettingsBox pinned under a 44 px header (position: absolute; top: 44px; bottom: 0), sections at height: 100%, scroll-snap-stop: always, overscroll-behavior: contain, 14 px text on a dark background for outdoor use, #4ade80 accent, a hard-coded “Next article ↓” line in each article; in production, a 100dvh box.

How it works

A .sn-vsnap-wrap frame in position: relative, #1e1b4b background, white text, font Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif. Inside it, the .sn-vsnap-container box is 100% × 280px, overflow-y: auto, scroll-snap-type: y mandatory and scroll-behavior: smooth. It holds four .sn-vsnap-section blocks 280px tall in box-sizing: border-box with scroll-snap-align: start: at the end of every gesture the browser finishes the scroll on the top of the nearest section — mandatory forbids any resting position in between. Each section is a centered flex column (gap: 8px, padding: 20px) with a white 0.9rem h4 and a 0.72rem p in white at 60%, on four 135° gradients running from #1e1b4b → #312e81 to #6d28d9 → #7c3aed, indigo to violet. After the box, a span.sdc-scroll-hint reading “Scrollez ↓” in position: absolute anchors to the frame (measured: offsetParent = .sn-vsnap-wrap, 8 px above the bottom of the box, centered) and pulses between 30% and 70% opacity every 2 s. The JS field of the sold code is a single comment: the scrolling engine does all the work.

The box-sizing: border-box line on .sn-vsnap-section is not decorative. Without it, the 20px padding would be added to the 280px and each section would measure 320 px inside a 280 px window (measured with the line removed: scrollHeight 1280, scroll range 1000 instead of 840, text 20 px below the visible center). When a snap area is taller than the snapport, the specification allows any position where the area still covers it: every section would have a 40 px free band inside which the snap holds without switching — a 100 px wheel notch would settle at 40, Arrow Down too, a 600 px notch would land at 640 and the bottom 40 px of every slide would be cut off. With the line, measured in Chromium with the pointer over the box: 280 px sections, 840 px range, text centered to the pixel; a 100 px notch climbs to 82 then returns to 0 (the snap goes to the nearest position: you must cover more than half a section, 140 px, in a single gesture — 150 px settles at 280), two quick 100 px notches make 280, a 600 px notch goes to 560, section 3, skipping section 2 (no scroll-snap-stop).

The gesture isn't read the same way everywhere. Measured on the sold code, blank page, box at rest: Chromium holds the 100 px notch (back to 0) where Firefox 153 and WebKit 26 advance one section (280); at 600 px, Chromium goes to 560, Firefox to 280 (one section per gesture, whatever the amplitude) and WebKit runs to the end of the range, 840. scroll-behavior: smooth only governs programmatic and anchor scrolls: a scrollTop = 280 reaches 280 in 274 ms in Chromium, 199 ms in WebKit and 579 ms in Firefox, and settles there with no overshoot; the wheel is eased by the engine's own snap animation. Two writing choices follow from the code being self-contained. The frame's #1e1b4b background is the start color of the first gradient: the gradients sit on the sections, the frame keeps the box dark on a white page and wherever a mail client doesn't render linear-gradient. The font is declared once, on the frame, and inherited by the headings, the paragraphs and the hint: Inter isn't loaded by the effect, so the browser falls through to the next link in the stack (measured: system-ui in Chromium) — load Inter in your page if you want it, or replace the stack with your own.

Accessibility

  • prefers-reduced-motion not handled: under the preference, scroll-behavior still computes to smooth (measured in Chromium: scrollTop = 280 still takes 271 ms) and the hint keeps pulsing (animation measured running). Add @media (prefers-reduced-motion: reduce) { .sn-vsnap-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }. The snap itself is not an animation but a resting position: it stays active, and that is acceptable (WCAG 2.3.3 targets motion, not alignment).
  • Keyboard: Chromium and Firefox give the box focus on Tab (measured: activeElement = .sn-vsnap-container), WebKit doesn't (focus stayed on body, Arrow Down did nothing). Add tabindex="0", an aria-label (“Four sections, vertical scrolling”) and a :focus-visible style — Chromium's default focus ring surrounds the whole box. Once the box has focus: Arrow Down = next section (measured 280 then 560), Arrow Up returns to 280, Page Down goes to 280, End to 840, Home to 0.
  • Contrast (11.5 px text, 4.5:1 threshold): the paragraph in white at 60% reads 6.5:1 on #1e1b4b but 3.5:1 on #6d28d9 and 3.0:1 on #7c3aed — section 4 fails. Raise the opacity to 80% (rgba(255,255,255,.8)) or lighten the last gradient less. The white titles pass everywhere (5.7:1 at worst). The hint at 30% reads 1.7 to 2.6:1, and 3.6 to 8.4:1 at the peak of its pulse: put aria-hidden="true" on it, it is decorative.
  • Screen readers and fallback: four divs with one h4 each — a level 4 with no h2/h3 above breaks the page outline; use the level that follows your current heading, or section aria-labelledby. mandatory forbids any rest between two sections: if a section may exceed the box height (text zoomed to 200%, variable content), prefer proximity — with mandatory, the bottom of a section taller than the box is only reachable inside the band where it still covers the window, and the snap holds every notch there (which is exactly what the box-sizing line prevents for the sections as shipped). With a finger, scrolling is the browser's own (no listener); overscroll-behavior: contain keeps the page from being dragged along when you reach the end of the box.

Browser compatibility

CSS only: scroll-snap-type and scroll-snap-align (modern syntax), scroll-behavior, overflow-y, flexbox, linear-gradient, @keyframes. Zero dependencies, zero JavaScript. This is classic CSS, not an animation-timeline-driven animation: the snap works in Firefox (measured in Firefox 153 — same geometry, same stops), whereas animation-timeline scroll effects work in no Firefox version at all.

Chrome 69+✓ Full
Firefox 68+✓ Full
Safari 15.4+✓ Full (11 to 15.3: snap without programmatic easing)
Edge 79+✓ Full
Mobile iOS 15.4+✓ Full (11+: snap only)
Android Chrome 69+✓ Full

Without snap support (before Chrome 69, Firefox 68, Safari 11), the box is still a 280 px area that scrolls freely: the sections follow each other with no automatic stop. Without scroll-behavior, only programmatic scrolls lose their easing — wheel and finger are unaffected. Where overflow is not honored (some mail clients), the four sections stack at their own height: everything stays readable, just not as slides; and where gradients aren't rendered, the frame's #1e1b4b background keeps the white text readable.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="sn-vsnap-wrap">
  <div class="sn-vsnap-container">
    <div class="sn-vsnap-section">
      <h4>Section 1</h4>
      <p>Faites défiler pour passer à la suivante</p>
    </div>
    <div class="sn-vsnap-section">
      <h4>Section 2</h4>
      <p>Le scroll s'arrête automatiquement</p>
    </div>
    <div class="sn-vsnap-section">
      <h4>Section 3</h4>
      <p>Expérience fluide et contrôlée</p>
    </div>
    <div class="sn-vsnap-section">
      <h4>Section 4</h4>
      <p>Dernière section</p>
    </div>
  </div>
  <span class="sdc-scroll-hint">Scrollez ↓</span>
</div>
CSS
/* Cadre de l'effet : son fond, sa police et l'ancre de l'indice « Scrollez ↓ ».
   Sans fond déclaré, le texte blanc disparaît sur une page claire. */
.sn-vsnap-wrap  {
   position: relative;
   width: 100%;
   background: #1e1b4b;
   color: #fff;
   font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
   }

.sn-vsnap-container  {
   width: 100%;
   height: 280px;
   overflow-y: auto;
   scroll-snap-type: y mandatory;
   scroll-behavior: smooth;
   }

.sn-vsnap-section  {
   box-sizing: border-box; /* le padding compte dans les 280 px : une section = une fenêtre */
   height: 280px;
   scroll-snap-align: start;
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
   gap: 8px;
   padding: 20px;
   text-align: center;
   }

.sn-vsnap-section:nth-child(1)  {
   background: linear-gradient(135deg, rgb(30, 27, 75), rgb(49, 46, 129));
   }

.sn-vsnap-section:nth-child(2)  {
   background: linear-gradient(135deg, rgb(49, 46, 129), rgb(76, 29, 149));
   }

.sn-vsnap-section:nth-child(3)  {
   background: linear-gradient(135deg, rgb(76, 29, 149), rgb(109, 40, 217));
   }

.sn-vsnap-section:nth-child(4)  {
   background: linear-gradient(135deg, rgb(109, 40, 217), rgb(124, 58, 237));
   }

.sn-vsnap-section h4  {
   font-size: 0.9rem;
   font-weight: 700;
   color: white;
   margin: 0px;
   }

.sn-vsnap-section p  {
   font-size: 0.72rem;
   color: rgba(255, 255, 255, 0.6);
   margin: 0px;
   }

.sdc-scroll-hint  {
   position: absolute;
   bottom: 8px;
   left: 50%;
   transform: translateX(-50%);
   font-size: 0.65rem;
   color: rgba(255, 255, 255, 0.3);
   z-index: 5;
   pointer-events: none;
   animation: 2s ease-in-out 0s infinite normal none running sdcHintPulse;
   }

@keyframes sdcHintPulse  {
   
  0%, 100%  {
   opacity: 0.3;
   }
  50%  {
   opacity: 0.7;
   }
}
JavaScript (fx-0593)
// Effet CSS pur — pas de JavaScript nécessaire

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Box and section height (CSS — .sn-vsnap-container, .sn-vsnap-section) 280px / 280px, padding included (box-sizing: border-box) Both must be equal for a section to fill the window; the section's border-box keeps the 20 px of padding inside the 280 px. To have the sections follow the box whatever its height, switch them to height: 100%. Measured without the border-box: 320 px sections, a 40 px free band per section, range 1000 instead of 840.
Frame background and font (CSS — .sn-vsnap-wrap) #1e1b4b, Inter then the system font The background is the start color of the first gradient: it keeps the white text readable on a light page and wherever gradients aren't rendered; match it to your first gradient if you change the colors. Inter isn't loaded by the effect: add it to your page or replace the stack with your site's.
scroll-snap-type (CSS — .sn-vsnap-container) y mandatory y proximity only holds near the top of a section and allows rest elsewhere — safer if a section may exceed the box. x mandatory with sections side by side gives the same snap horizontally.
scroll-snap-align (CSS — .sn-vsnap-section) start center aligns the section's center with the box's (useful when sections are smaller than the box), end its bottom.
scroll-snap-stop (CSS — to add on .sn-vsnap-section) absent (normal) always forbids skipping a section in one gesture: measured without it, a 600 px notch goes from section 1 to 3. It is the only CSS difference from Smooth Snap (fx-0598).
scroll-behavior (CSS — .sn-vsnap-container) smooth Only eases programmatic scrolls (scrollTo, scrollTop, anchors): measured 300 ms per section in Chromium, instant with auto. Wheel and finger are eased by the engine regardless.
Number and backgrounds of sections (HTML + CSS — :nth-child(1) to (4)) 4 sections, 4 gradients at 135° A fifth section has no background: add its :nth-child(5) rule or replace the four with a single color. The gradients run from #1e1b4b to #7c3aed; from #6d28d9 on, the 60% paragraph drops below 4.5:1.
“Scrollez ↓” hint (CSS — .sdc-scroll-hint) 8 px from the bottom, centered, 2 s pulse from 30 to 70% Anchors to the .sn-vsnap-wrap frame (position: relative): measured 8 px above the bottom of the box, centered, over the visible section. To move it, change bottom and left; to remove it, delete the span — the @keyframes sdcHintPulse goes with it.
scroll-padding-top (CSS — to add on .sn-vsnap-container) absent Moves the stop below a sticky header: scroll-padding-top: 56px for a 56 px bar, otherwise it covers the top of every section.

FAQ

Why doesn't one wheel notch move to the next section?

Because the snap is computed at the end of the gesture, toward the nearest valid position, and a 100 px notch is less than half of a 280 px section. Measured in Chromium: 100 px → 82 → back to 0; 150 px → 280; two quick 100 px notches → 280. You need more than 140 px in a single gesture (one trackpad flick is enough). Engines differ: Firefox 153 and WebKit 26 advance one section from the very first 100 px notch (measured 280), and at 600 px Firefox stays at 280 while WebKit runs to 840. For a “one notch = one section” behavior everywhere you need JavaScript (listen to wheel and call scrollTo) — which is precisely what this effect doesn't do.

How does it differ from Smooth Snap (fx-0598), Scroll Snap Sections (fx-0584) and Snap + Progress (fx-0597)?

The CSS core is the same in all four: 280 px box, overflow-y: auto, scroll-snap-type: y mandatory, scroll-behavior: smooth, sections in scroll-snap-align: start. Smooth Snap adds scroll-snap-stop: always and an IntersectionObserver (0.6 threshold) that puts .in-view on the visible section to animate its icon and title. Scroll Snap Sections adds a scroll listener that computes Math.round(scrollTop / 280) and lights the matching dot. Snap + Progress adds a width-based progress bar, five clickable dots and a programmatic scrollTo. Vertical Snap is the bare base: pick it when JavaScript isn't available (email, static generator, kiosk) or when you will write the indicator yourself; pick a neighbor when you want its dots or animations without coding them.

Can the snap apply to the whole page instead of a 280 px box?

Yes: put scroll-snap-type: y mandatory on html (the page's scroll container) and give the sections min-height: 100vh100dvh on mobile, to follow the address bar — with scroll-snap-align: start. Three precautions: scroll-padding-top equal to the height of a fixed header, otherwise it covers the top of every section; proximity rather than mandatory as soon as a section may exceed the screen (zoomed text, small landscape screen), since the page will no longer be able to rest in the middle; and, if you keep the “Scrollez ↓” hint, switch it to position: fixed — anchored to the frame, it would sink to the bottom of the document, not of the screen. The wheel is no “stricter” than in a box: measured, the same nearest-position snap applies.