← The library

Snap sections

Panels that each stop on their own edge

Free

What's included

A container of sections that snap as you scroll: each panel fills the frame exactly and stops on its own edge, with a bar on the right saying where you are. Two files, a stylesheet and a script. No dependencies, no build step, no external requests.

The snapping itself is native CSS (scroll-snap-type): the wheel, the trackpad, the finger and the browser's own scrollbar carry on doing what they do everywhere else. The script only draws the bar and makes the keyboard usable.

Features

  • Works without JavaScript — the snapping, the full frame and the scrolling are all native. Until the script has run, the browser's own scrollbar stays put: a frozen indicator lying about the position would be worse than no indicator at all.
  • Full keyboard support — arrows, Page Up, Page Down, Home, End and the space bar move from one panel to the next. This isn't a convenience: under mandatory, an arrow key moves forty pixels that the snap immediately pulls back, so without this the component can't be driven from the keyboard at all.
  • A status line for screen readers — “Panel 2 of 4” on every change. The label is an attribute, not a string inside the script: it can be translated without touching the code.
  • One variable for the scale — bar width, corner radius and text size all derive from --ss-font-size. The bar grows with the rest instead of staying a 6 px line.
  • As many panels as you like — the thumb's height is 100 % / N, counted by the script. Add or remove a panel later and it recounts itself.
  • Reduced motion, forced colours — under prefers-reduced-motion, the fade-in and the smooth jumps fall to zero; under forced-colors, the bar takes on the system colours.

Dropping it into a page

Two files to load, then the markup:

<link rel="stylesheet" href="snap-sections.css">
<script src="snap-sections.js" defer></script>
<div class="snap-sections" data-ss-status="Panel %n of %total">
  <div class="snap-sections__viewport" tabindex="0"
       role="group" aria-label="Snapping panels">
    <section class="snap-sections__panel">First</section>
    <section class="snap-sections__panel">Second</section>
    <section class="snap-sections__panel">Third</section>
  </div>
  <div class="snap-sections__rail" aria-hidden="true">
    <div class="snap-sections__thumb"></div>
  </div>
  <p class="snap-sections__status" role="status"></p>
</div>

One thing not to forget: the component is 100% of its parent's height and doesn't invent one. So it's the page that says how tall the stage is — height: 100svh for full screen, a fixed height for an inset in the middle of an article.

What you should know

  • The bar sits over the content. It's placed on the right, hard against the edge, with no column of its own — as on the reference. So the last six pixels of a panel pass underneath it. If its content needs to clear it, give the panel some right-hand padding.
  • A single wheel click doesn't change panel. The snapping is strict: a scroll shorter than half a panel is pulled back to where it started. A continuous gesture passes without noticing — the browser adds the clicks up — but a mouse with firm detents needs a decisive push. That's the behaviour of native CSS, not of this component; the keyboard always moves a whole panel.
  • Past the last panel, the page carries on. The scroll is neither captured nor hijacked: that's the browser's behaviour, and the reference's. An overscroll-behavior: contain would shut it in, but that isn't what the original does.
  • One flaw in the reference has been fixed. On the original, the thumb only travels its own height: with four panels it stops a quarter of the way down the bar when you've reached the bottom. Here it travels the whole run. The original behaviour is still available through a variable, --ss-thumb-travel: 100%, and the README explains where the mistake comes from.
  • No inline styles, no inline scripts. The component honours the site's strict security policy and never uses innerHTML: the thumb's position goes through a custom property, which the CSSOM accepts where a <style> would be refused.
  • The design comes from elsewhere. The proportions and the behaviour follow those of a published Framer component, measured on the live site — the boxes land within a hundredth of a pixel. The code itself is original: it's ordinary CSS and JavaScript where the original leans on React and an animation library.

Take it, open it, break it.

This component is free. The full library — the templates and the other components — comes with the subscription, alongside the course.

Immediate access · No commitment · Cancel in one click