Mixed Content Snap
A 280 px viewport with scroll-snap-type: y — proximity, not mandatory — and five sections of 280, 140, 140, 80 and 280 px aligned to start: snap points at 0, 280, 420, 560 and 640 px, a section shorter than the viewport never fills it alone, and one wheel notch from the top stops at 100 px, between two. Zero JavaScript.
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
The sold HTML is a .demo-preview (centered flex, min-height: 300px, position: relative, #0a0a0f background) holding .sn-mixed-container — width: 100%, height: 280px, overflow-y: auto, scroll-snap-type: y, scroll-behavior: smooth — and, next to it, the .sdc-scroll-hint cue “Scrollez ↓”: absolute, 8 px from the bottom of the .demo-preview, 10.4 px, white at 30%, opacity pulsing from .3 to .7 over 2 s (sdcHintPulse). Inside the container, five .sn-mixed-section with scroll-snap-align: start, centered flex column, padding: 16px, whose height comes from a second class: .sn-full 280 px (exactly the viewport), .sn-half 140 px, .sn-quarter 80 px. The sold order — full, half, half, quarter, full — makes 920 px of content for 640 px of travel. Backgrounds are five 135° gradients assigned by :nth-child(1) to (5), from #1e1b4b to #8b5cf6: they follow the rank, not the size. Text: h4 .85 rem (13.6 px) white, p .68 rem (10.9 px) white at 60%; every h4 carries an inline font-family: Inter, …, a catalog leftover.
The snap points are the section tops: 0, 280, 420, 560 and 640 px — the last one coincides with the end of travel because the final section is as tall as the viewport. This is where mixed heights change the reading: a section shorter than the viewport never fills it alone. At 280, the two half sections share the screen; at 420, the second half, the quarter and 60 px of the final one; at 560, the quarter takes the top and the final section the remaining 200 px. The rhythm is the gap between stops: 280, 140, 140, 80. And scroll-snap-type: y with no second keyword means proximity, not mandatory: snapping only acts within reach of a point. Measured in Chromium 151: a position left 93 px from a point is pulled to it, at 94 px it stays — the threshold is one third of the viewport (280 / 3). Consequence: one wheel notch (100 px) from the top stops at 100 px and stays there (still at 100 px two seconds later); two notches within 120 ms reach 280; after that every notch lands within reach of a point: 280 → 420 → 560 → 640. On a trackpad, 5 × 20 px are pulled back to 0, 13 × 20 px stop at 143 px, in between.
scroll-behavior: smooth only concerns programmatic scrolls (scrollTo, anchors, keys): measured 0 → 640 px in 395 ms, 226 px covered at 100 ms. No JavaScript: no listener, nothing to remove, the snap animation is the browser's. Three limits belong to the code itself. The :nth-child backgrounds: a sixth section has none (measured background: none). The last section: switched to .sn-half, its top (640) exceeds the travel (500) and it never aligns to the top (measured: stop at 500). The cue: it overlaps the container's last 10 pixels (280–292 px versus 10–290) and, without the positioned .demo-preview, drops to the bottom of the window (measured at 479 px for a container ending at 280). No site CSS variable, no console error.
Accessibility
- prefers-reduced-motion not handled: under
reduce,scroll-behavior: smoothstays active (measured: 23 px covered 50 ms after ascrollTo) and the cue keeps pulsing (playState: running). Scrolling is reader-initiated and the snap animation is the browser's, but add@media (prefers-reduced-motion: reduce) { .sn-mixed-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }. One upside of proximity mode: no content is unreachable, unlikemandatory, which can hold the reader between two points. - Keyboard: the container is a
divwithouttabindex. Recent Chromium builds and Firefox make scrollable areas with no focusable child reachable from the keyboard (measured in Chromium 151: Tab stops on it, Down arrow 40 px per press with no snapping — 40, 80, 120, 160 —, PageDown 420, Space 280, End 640), Safari does not. Settabindex="0",role="region"and anaria-labelon.sn-mixed-container. - Contrast measured on the screenshot pixels: white
h4from 14.0:1 (section 1) to 5.1:1 (section 5) — compliant;pat 60% white: 6.0 · 5.1 · 4.2 · 3.3 · 2.8:1 — sections 3 to 5 fall below 4.5:1, and at 10.9 px the text is not “large”. Raisergba(255, 255, 255, .6)to.85(≥ 4.5:1 everywhere) or lighten the last gradients less. The cue (10.4 px, 30% white) measures 2.6:1 on#0a0a0fand 2.5:1 on the section it overlaps: decorative, set itaria-hidden="true"— and translate “Scrollez ↓”, which is hard-coded. - Sizes and structure: 13.6 px for headings, 10.9 px for paragraphs — catalog-card sizes, raise them to 14–16 px in a page. Headings are
h4with noh1–h3above: adjust the level to your outline. Sections aredivs: for a screen reader the rhythm of heights does not exist, the hierarchy must also live in the headings. On touch, proximity snapping works and the 280 px container lets the gesture continue onto the page at the end of travel (defaultoverscroll-behavior): no scroll trap.
Browser compatibility
CSS only: scroll-snap-type / scroll-snap-align (modern syntax), overflow-y: auto, linear-gradient, @keyframes; scroll-behavior: smooth as a comfort. No script, no library, no CSS variable. This is classic CSS, not an animation driven by animation-timeline: Firefox renders it fully, whereas it supports native scroll-driven animations in no version at all.
Without scroll-snap (older browsers), the container is a plain scroll box: the five sections keep their heights and backgrounds, everything stays readable, nothing snaps. Without scroll-behavior (Safari before 15.4), programmatic jumps are instant; touch and wheel scrolling do not depend on it. Without JavaScript: identical, there is none.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<div class="sn-mixed-container">
<div class="sn-mixed-section sn-full">
<h4>Main section</h4>
<p>Full height</p>
</div>
<div class="sn-mixed-section sn-half">
<h4>Medium section</h4>
<p>Half height</p>
</div>
<div class="sn-mixed-section sn-half">
<h4>Medium section</h4>
<p>Half height</p>
</div>
<div class="sn-mixed-section sn-quarter">
<h4>Small</h4>
<p>Compact</p>
</div>
<div class="sn-mixed-section sn-full">
<h4>Final section</h4>
<p>Back to full height</p>
</div>
</div>
<span class="sdc-scroll-hint">Scroll ↓</span>
</div>
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 |
|---|---|---|
Snap mode (CSS — .sn-mixed-container scroll-snap-type) |
y (= y proximity) |
In proximity, snapping only acts within a third of the viewport of a point (93 px): one wheel notch from the top stays at 100 px. y mandatory always returns to the nearest point — safe here, no section exceeds the viewport. |
Viewport height (CSS — .sn-mixed-container and .sn-full) |
280 px / 280 px | The two go together: a full section must be exactly the viewport to fill it alone, and the proximity threshold follows (one third). In a page, 100vh for both; in a component, a fixed value. |
Steps (CSS — .sn-half, .sn-quarter) |
140 px / 80 px | These are the gaps between stops. Add classes (.sn-third at 93 px). A gap below the threshold chains two stops in one notch; a 280 gap leaves a dead zone from 94 to 186 px where a notch stops in between. |
Alignment (CSS — .sn-mixed-section scroll-snap-align) |
start |
center centers a short section in the viewport with its neighbors on either side; end on the last section makes it reachable even when shorter than the viewport (its bottom aligns with the bottom of the frame). |
| Order and last section (HTML) | full · half · half · quarter · full | The last snap point must stay ≤ the travel (content − 280). End with a .sn-full, or give the last section scroll-snap-align: end, or add a padding-bottom to the container. |
Backgrounds (CSS — .sn-mixed-section:nth-child(n)) |
5 gradients #1e1b4b → #8b5cf6, by rank | Tied to position, not to the size class: the sixth section has no background. Move them onto .sn-full / .sn-half / .sn-quarter so that color tells height. |
Smoothing (CSS — scroll-behavior) |
smooth |
Animates scrollTo, anchors and keys (395 ms for 640 px measured), not the wheel or the finger. Switch to auto under prefers-reduced-motion. |
Cue (HTML/CSS — .sdc-scroll-hint) |
“Scrollez ↓”, 8 px from the bottom, 2 s pulse | Requires a position: relative parent and overlaps 10 px of the container. Raise it (bottom: 18px), translate it, or remove it together with its @keyframes. |
FAQ
scroll-snap-type: y with no second keyword means proximity: the browser only snaps if the scroll stops within reach of a point — in Chromium, one third of the viewport, i.e. 93 px here (measured: 93 px pulled back, 94 px left alone). A wheel notch is 100 px: from the top it stops at 100 px, 100 px from point 0 and 180 px from point 280, out of reach of both. The second notch reaches 200 px, 80 px from point 280, and snapping kicks in; after that every notch lands within reach (280 → 420 → 560 → 640). For a clean stop on every notch, write scroll-snap-type: y mandatory — here no section exceeds the viewport, nothing becomes unreachable. Keeping proximity is also a choice: the reader can pause in the middle of a full section.y mandatory — each stop shows exactly one section, the points are all 280 px apart. Here the points are 280, 140, 140 and 80 px apart, the mode is proximity, and three stops out of five show several sections at once: height becomes a signal of weight. Snap + Progress adds a bar and navigation dots in JavaScript, but its script computes the current section with Math.round(scrollTop / clientHeight) — it assumes equal sections. Wired to these heights, it would give index 1 to stops 280 and 420 and index 2 to stops 560 and 640: two dots out of five never lit. For navigation here, compare scrollTop with the sections' actual offsetTop.scrollHeight − clientHeight. With the last section as .sn-half, the content is 780 px, the travel 500, and that section's top is at 640: its snap point lies beyond the end (measured: stop at 500, the section stays stuck at the bottom of the frame, below the quarter). The sold code avoids the issue by ending with a .sn-full. Three ways out if your last section must be short: scroll-snap-align: end on it (its bottom aligns with the bottom of the frame — the setting used by the three examples), a padding-bottom on the container equal to the difference (140 px for a half), or an empty closing section.