Orchestrate magnetic scrolling, browser-animated state transitions and rigorous respect for prefers-reduced-motion.
Open this lesson in KodokonScroll 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.
.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;
}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.
function applyUpdate(updateDom) {
if (!document.startViewTransition) {
updateDom();
return;
}
document.startViewTransition(() => updateDom());
}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.
.hero-thumb { view-transition-name: hero; }
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}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.
const reduceMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (reduceMotion || !document.startViewTransition) {
updateDom();
} else {
document.startViewTransition(() => updateDom());
}