Native CSS Pinning That Beats Wholesale JavaScript Libraries
Why Scroll-Snap Beats JavaScript Pinning
JavaScript pinning libraries work by listening to scroll events, calculating offsets, and applying transforms or position changes to hold sections in place. This happens on every frame — 60 times a second — and threads through the main JavaScript thread, which also handles interactions, animations, and user input. If the user's device is busy or the library code is heavy, scroll becomes janky. The browser's native scroll-snap implementation runs on the GPU, bypasses the main thread entirely, and guarantees smooth 60fps snapping. Plus, it's progressive: browsers without scroll-snap support still scroll normally — no fallback library needed.
The Markup: Full-Height Sections with Snap Points
Wrap your sections in a vertical flex container with `scroll-snap-type: y mandatory`. Each full-height section gets `scroll-snap-align: start` and `height: 100vh` so the browser snaps the top of each section to the top of the viewport as you scroll. The container needs `overflow-y: auto` and `scroll-behavior: smooth` for buttery motion. That's it.
{`.scroll-container {
display: flex;
flex-direction: column;
height: 100vh;
overflow-y: auto;
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
}
.pinned-section {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
flex-direction: column;
justify-content: center;
}`}The `scroll-snap-stop: always` property forces the browser to stop at each section instead of coasting past. On fast scrolls or trackpad momentum, users can't accidentally skip sections.
The Money Pattern: Layered Content Inside Pinned Sections
Each section is a full-height canvas. Stack content inside with flex or CSS grid — hero image, text overlay, call-to-action — and let the snap container handle the positioning. Velocity X layers a background image, a semi-transparent overlay, headline copy, and a button inside each section. When the user scrolls, the section snaps and stays centred until they scroll again. No JavaScript calculates offsets. No library manages state. The browser does it all.
{`.pinned-section {
background: url('/case-study-hero.jpg') center / cover;
position: relative;
}
.pinned-section::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
}
.pinned-section-content {
position: relative;
z-index: 10;
max-w-2xl;
text-white;
}
.pinned-section:nth-child(2) { background-image: url('/case-2.jpg'); }
.pinned-section:nth-child(3) { background-image: url('/case-3.jpg'); }`}The Catch: Older Browsers and Non-Snappy Devices
Scroll-snap shipped in 2016, so IE11 is out. Most phones and desktops from 2018+ support it. If you need IE support, you'll still need a polyfill — but for everyone else (99% of traffic), the native implementation is bulletproof. And if scroll-snap fails, the page still scrolls; sections just don't pin. No broken experience, just a graceful fallback to normal scroll behaviour.
The Verdict
Scroll-snap is production-ready and faster than any JavaScript alternative. If you're pinning sections on a marketing site, stop reaching for ScrollTrigger and start with CSS. See the full pattern in Velocity's `/pricing` page, then port it to your site. The performance win is immediate and the code weight saved is permanent.