Kodokon kodokon.com

Modern interactions: scroll-snap, view transitions

Orchestrate magnetic scrolling, browser-animated state transitions and rigorous respect for prefers-reduced-motion.

11 min · 3 questions

Open this lesson in Kodokon

Scroll snap involves two actors: the container defines a snapport (its visible area, shrunk by scroll-padding) and a strategy via scroll-snap-type - proximity only snaps near a snap point, while mandatory guarantees you always land on one, even after programmatic scrolling. The children declare their alignment area with scroll-snap-align (adjustable via scroll-margin). A little-known subtlety: during a fast swipe, the browser may legitimately skip several snap points; scroll-snap-stop: always forces it to stop on each one, essential for a story-style carousel.

CSS
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 1rem;
}
.carousel > article {
  flex: 0 0 80%;
  scroll-snap-align: center;
  scroll-snap-stop: always;
}
A magnetic carousel: every slide centers itself and no swipe can skip one.

The View Transitions API animates a DOM state change without any library. document.startViewTransition(callback) captures a visual snapshot of the current state, runs your callback (which mutates the DOM), then animates between the old and new states. Under the hood, the browser builds a tree of pseudo-elements on top of the page: ::view-transition contains ::view-transition-group elements, each holding a ::view-transition-old (the snapshot, replaced content comparable to an image) and a ::view-transition-new (the live representation). The default: a cross-fade. The page is non-interactive during the transition.

JAVASCRIPT
function applyUpdate(updateDom) {
  if (!document.startViewTransition) {
    updateDom();
    return;
  }
  document.startViewTransition(() => updateDom());
}
Progressive enhancement: without support, the update simply applies with no animation.

The real power comes from view-transition-name: a named element is extracted into its own transition group. If an element with the same name exists before and after, the browser automatically interpolates position and size between the two - the morphing effect from a thumbnail to its full-screen view, without a single line of animation JavaScript. Each name must be unique on the page at any given moment, or the transition is abandoned. The pseudo-elements are styled with ordinary CSS animations.

CSS
.hero-thumb { view-transition-name: hero; }

::view-transition-old(hero),
::view-transition-new(hero) {
  animation-duration: 0.4s;
  animation-timing-function: ease-in-out;
}
The named hero group is intercepted and its animation duration customized.

That leaves the accessibility obligation: prefers-reduced-motion: reduce signals a user sensitive to motion (vestibular disorders, migraines). Reducing does not mean removing: an opacity fade remains acceptable; it is the large movements, zooms and parallax effects that must be neutralized. You already know the defensive global override in CSS (near-zero durations, covered in the previous module); view transitions add a second front: on the JavaScript side, query window.matchMedia("(prefers-reduced-motion: reduce)") before launching a transition or an animated scroll.

JAVASCRIPT
const reduceMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (reduceMotion || !document.startViewTransition) {
  updateDom();
} else {
  document.startViewTransition(() => updateDom());
}
The JavaScript safeguard: the update applies without morphing when the user asks for less motion.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What does scroll-snap-stop: always do on a carousel item?
    • It disables touch scrolling inertia
    • It forces a stop on every snap point, even during a fast swipe
    • It blocks scrolling along the perpendicular axis
  2. What does document.startViewTransition() capture before running your callback?
    • A deep clone of the current DOM
    • A visual snapshot of the old state, replayed in ::view-transition-old
    • The serialized HTML of the entire page
  3. How do you respect prefers-reduced-motion before launching a view transition from JavaScript?
    • You can't: this preference is only readable in CSS
    • Test window.matchMedia('(prefers-reduced-motion: reduce)').matches and apply the update without startViewTransition when it matches
    • Pass the { motion: 'reduce' } option to startViewTransition