Smooth Scrolling and scroll-behavior Under Reduced Motion
Part of Vestibular-Safe Motion Patterns in Accessible Motion Architecture.
The problem
A documentation site sets html { scroll-behavior: smooth; } so table-of-contents links glide to their sections. It feels polished. For a user with a vestibular disorder, clicking “Frequently asked questions” at the top of a long page sends the entire viewport sliding through thousands of pixels of text in under a second — full-field motion they did not initiate by scrolling and cannot control, exactly the kind of movement that triggers dizziness and nausea.
The site does have a reduced-motion stylesheet. It disables animations and transitions. It does not touch scroll-behavior, and even if it did, the “back to top” button and the search result highlighter call scrollIntoView({ behavior: 'smooth' }) from script, which ignores CSS entirely.
Root cause analysis: scrolling is full-field motion
Most reduced-motion guidance targets animated elements. Programmatic smooth scrolling is different in kind: it moves everything in the viewport at once, over a distance set by the page rather than by the user’s hand. Vestibular symptoms are driven by large areas of the visual field moving, which is why parallax and full-screen slides are high-risk; a long smooth scroll is the same stimulus.
User-driven scrolling is different. When a person scrolls with a wheel, trackpad or touch, the motion is coupled to their own movement and stops when they stop, which the visual system and inner ear can reconcile. Reduced-motion guidance does not ask sites to stop scrolling — it asks them not to generate movement on the user’s behalf.
Two technical details cause most implementations to miss cases. First, scroll-behavior is not an animation or transition, so rules that zero animation-duration and transition-duration do not affect it. Second, the behavior option passed to scrollTo(), scrollBy() and scrollIntoView() takes precedence: 'smooth' animates regardless of CSS, 'instant' jumps regardless of CSS, and only 'auto' defers to the element’s computed scroll-behavior.
Step-by-step resolution
Production code pattern
/* Smooth scrolling only for users who have not asked for reduced motion. */
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
html {
scroll-padding-top: calc(var(--header-height) + 1rem); /* anchors land below the sticky header */
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; } /* explicit, in case a later rule sets smooth */
}
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
const scrollMode = () => (reducedMotion.matches ? 'auto' : 'smooth');
// One helper for every programmatic scroll in the app.
export function scrollToSection(target) {
target.scrollIntoView({ behavior: scrollMode(), block: 'start' });
// Move keyboard focus without triggering a second scroll.
if (!target.hasAttribute('tabindex')) target.setAttribute('tabindex', '-1');
target.focus({ preventScroll: true });
}
// Back to top: instant for reduced motion, smooth otherwise.
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: scrollMode() });
document.querySelector('main h1')?.focus({ preventScroll: true });
});
Rendering Impact: composite during smooth scrolling, none for an instant jump. Smooth scrolling is performed by the browser’s scroll machinery, usually off the main thread; the concern here is vestibular, not performance.
auto rather than instant in the helper is deliberate: it defers to the CSS, so the one place that decides whether scrolling is smooth is the stylesheet. scroll-behavior applies to scroll containers, not to the links or targets, so any override — for example a nested panel that should always jump — belongs on that scrolling element.
Focus handling matters because a smooth scroll without a focus move leaves keyboard users where they started. Pressing Tab after clicking a table-of-contents link would take them to the next link in the table of contents, not into the section they scrolled to. preventScroll: true stops the focus call from starting a second scroll.
Verification checklist
Constraints and trade-offs
- Some users like smooth scrolling as an orientation aid; gating it on the preference keeps it for them while removing it for those who asked.
- Instant jumps can disorient on very long pages; updating the URL hash and moving focus gives users a way to understand where they landed.
- Browsers may apply their own smooth scrolling for keyboard Page Down and Space, independent of CSS; that is user-initiated and outside the page’s control.
- Scroll-snap containers can animate snapping after an instant jump in some engines; test snapped carousels separately.
scroll-padding-topmust be updated if the header height changes responsively; a custom property keeps it in sync.
Frequently asked questions
Does prefers-reduced-motion disable scroll-behavior: smooth automatically?
No. Browsers do not change scroll-behavior based on the preference. You need to set smooth scrolling only inside a no-preference media query, or reset it to auto under reduce.
Why does my reduced-motion CSS not stop scrollIntoView from animating?
An explicit behavior: ‘smooth’ option in script overrides the CSS scroll-behavior property. Pass behavior: ‘auto’ so the call follows CSS, or choose the value in script from the media query.
Is user scrolling with a trackpad a reduced-motion concern?
Not for the page to control. Motion the user produces directly is coupled to their own movement. The concern is movement the page generates for them, such as smooth programmatic scrolling and scroll-jacking.
Should I disable smooth scrolling for everyone?
It is not required, but it is a reasonable default for long documents, where smooth jumps cover large distances. Gating it on the preference is the minimum.
Related
- Vestibular-Safe Motion Patterns — the parent topic on full-field motion
- Detecting prefers-reduced-motion in JavaScript — the media query listener the helper relies on
- Tuning animation-range for Sticky Headers — the header whose height the scroll padding tracks