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.

Which setting wins for each way of scrollingScript calls with an explicit smooth behaviour bypass every stylesheet.Which setting wins for each way of scrollingFollows CSS scroll-behavior?Reduced-motion safe by default?Anchor link to #idYesOnly if CSS is gatedscrollIntoView() withno optionsYesOnly if CSS is gatedscrollTo({ behavior:'auto' })YesOnly if CSS is gatedscrollTo({ behavior:'smooth' })NoNoUser wheel, touch orkeyboard scrollKeyboard onlyYes
Script calls with an explicit smooth behaviour bypass every stylesheet.

Step-by-step resolution

Making every programmatic scroll respect the preferenceGate the CSS once, then route script scrolling through one helper.Making every programmatic scroll respect the preference1Remove unconditional scroll-behavior: smooth from global styles.No smooth scrolling by default2Re-add it inside @media (prefers-reduced-motion: no-preference).Opt-in only for users who have not asked for less motion3Replace behavior: 'smooth' in script with a helper that returns auto or smooth from the mediaquery.Script and CSS agree4Set scroll-padding-top on the scroller equal to the sticky header height.Targets land below the header5After scrolling, move focus to the target with preventScroll.Keyboard position matches visual position6Audit third-party widgets that scroll the page, such as consent banners and chat.
Gate the CSS once, then route script scrolling through one helper.

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.

A table-of-contents jump for two usersSame click, same destination; only the journey differs.A table-of-contents jump for two usersNo motion preferencePage glides to the sectionHeader offset respectedFocus moves into the sectionSmooth, orientedprefers-reduced-motion: reducePage jumps to the sectionHeader offset respectedFocus moves into the sectionInstant, oriented
Same click, same destination; only the journey differs.

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-top must 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.