Fix: CSS Scroll Behavior Not Working — smooth scroll, scroll-snap, or scroll-margin Ignored
Quick Answer
How to fix CSS scroll issues — scroll-behavior: smooth not working, scroll-snap alignment problems, overflow container conflicts, scroll-margin for fixed headers, and browser compatibility.
The Problem
scroll-behavior: smooth is set but the page still jumps instantly:
html {
scroll-behavior: smooth;
}Or scroll-snap-type is set on the container but items don’t snap:
.carousel {
overflow-x: scroll;
scroll-snap-type: x mandatory;
}
.carousel-item {
scroll-snap-align: start; /* Items don't snap */
}Or anchor links scroll to the right position but content is hidden under a fixed header:
<a href="#section-2">Go to Section 2</a>
<!-- Section title ends up behind the navbar -->
<h2 id="section-2">Section 2</h2>Or programmatic scrolling with scrollTo ignores smooth behavior:
window.scrollTo({ top: 500, behavior: 'smooth' });
// Jumps instantly despite the CSS settingWhy This Happens
CSS scroll behavior has several environment-dependent rules:
prefers-reduced-motion— browsers and operating systems can disable smooth scrolling when users have enabled “Reduce Motion” in accessibility settings. If a user has this on,scroll-behavior: smoothis intentionally overridden.- The scrolling container must have
overflow—scroll-snap-typeonly works on the element that is actually scrolling. If the scroll happens on<html>or a parent, settingscroll-snap-typeon a child has no effect. scroll-snap-alignrequires defined dimensions — snap items must have explicit width/height (orflex/gridsizing) to snap correctly. Percentage sizes without a defined parent dimension fail silently.- Fixed/sticky headers obscure anchor targets — when you navigate to
#section, the browser scrolls theidelement to the top of the viewport, placing it directly behind a fixed header. - JavaScript
scrollTovs CSSscroll-behavior—scroll-behaviorin CSS affects scrolling triggered by anchor navigation andscrollTo()withbehavior: 'smooth'. Without passingbehavior: 'smooth'toscrollTo(), it jumps regardless of CSS.
Fix 1: Ensure scroll-behavior Applies to the Right Element
scroll-behavior: smooth must be on the scrolling element — usually html for page-level scrolling:
/* Most common setup — applies to anchor links and history navigation */
html {
scroll-behavior: smooth;
}
/* If your page scrolls inside a container, not <html> */
.scroll-container {
height: 100vh;
overflow-y: auto;
scroll-behavior: smooth;
}Respect user preferences:
/* Always include this to avoid vestibular disorders for users with prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Or use the modern approach: opt-in rather than opt-out */
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}JavaScript scrollTo — always pass behavior: 'smooth' explicitly:
// WRONG — CSS scroll-behavior doesn't affect this
window.scrollTo(0, 500); // Always instant
// CORRECT — pass behavior explicitly
window.scrollTo({ top: 500, behavior: 'smooth' });
// Scroll an element into view
document.getElementById('section-2').scrollIntoView({ behavior: 'smooth' });
// Scroll a container
document.querySelector('.scroll-container').scrollTo({
top: 300,
behavior: 'smooth'
});Fix 2: Fix scroll-snap Not Snapping
For scroll snap to work, the snap container and snap items must be set up correctly:
/* WRONG — scroll-snap-type on non-scrolling element */
.wrapper {
scroll-snap-type: x mandatory; /* .wrapper doesn't scroll */
}
.carousel {
overflow-x: scroll; /* This is the scrolling element */
}
/* CORRECT — scroll-snap-type on the element that scrolls */
.carousel {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Remove scrollbar bounce/rubber-band that interferes on iOS */
-webkit-overflow-scrolling: touch;
}
.carousel-item {
flex: 0 0 100%; /* Full width, no shrinking */
scroll-snap-align: start;
}mandatory vs proximity:
/* mandatory — always snaps to an item, even mid-scroll */
scroll-snap-type: x mandatory;
/* proximity — only snaps if the scroll position is close to a snap point */
scroll-snap-type: x proximity;
/* Both axes */
scroll-snap-type: both mandatory;Vertical scroll snap for full-page sections:
html, body {
height: 100%;
margin: 0;
}
.sections-container {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
}
.section {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always; /* Forces stop at each section, prevents skipping */
}scroll-snap-stop: always prevents users from scrolling past a snap point in one swipe gesture — useful for carousels where skipping items is undesirable.
Fix 3: Fix Anchor Links Hidden Under Fixed Headers
When navigating to an anchor, the target scrolls to the top of the viewport — under a fixed header. Fix this with scroll-margin-top:
/* Add scroll margin equal to header height */
:target {
scroll-margin-top: 80px; /* Match your header height */
}
/* Or apply to all headings that are anchor targets */
h1, h2, h3, h4, h5, h6 {
scroll-margin-top: 80px;
}
/* Use CSS custom property for easy maintenance */
:root {
--header-height: 64px;
}
[id] { /* All elements with an id */
scroll-margin-top: calc(var(--header-height) + 16px); /* + extra breathing room */
}Dynamic header height with JavaScript:
// Measure the header and update the CSS variable
function updateScrollMargin() {
const header = document.querySelector('header');
const height = header.getBoundingClientRect().height;
document.documentElement.style.setProperty('--header-height', `${height}px`);
}
updateScrollMargin();
window.addEventListener('resize', updateScrollMargin);For SPA anchor navigation where the URL changes but the element is already in the DOM:
// Smooth scroll to anchor, accounting for header offset
function scrollToAnchor(id) {
const el = document.getElementById(id);
if (!el) return;
const headerHeight = document.querySelector('header').offsetHeight;
const elementTop = el.getBoundingClientRect().top + window.scrollY;
window.scrollTo({
top: elementTop - headerHeight - 16, // 16px extra breathing room
behavior: 'smooth'
});
}Fix 4: Fix scroll-snap on Mobile and iOS
Mobile browsers have additional quirks with scroll snap:
/* iOS Safari: must explicitly set overflow on touch devices */
.carousel {
overflow-x: scroll;
-webkit-overflow-scrolling: touch; /* Enable momentum scrolling on iOS */
scroll-snap-type: x mandatory;
/* Hide scrollbar on WebKit without affecting scrollability */
scrollbar-width: none; /* Firefox */
}
.carousel::-webkit-scrollbar {
display: none; /* Chrome, Safari */
}
.carousel-item {
scroll-snap-align: center;
/* Items must have explicit width — percentage won't work without a flex parent */
width: 100%;
flex-shrink: 0;
}Padding inside snap containers:
/* scroll-padding-* on the container offsets where snapping lands */
.carousel {
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-padding-left: 24px; /* First item snaps 24px from the left edge */
}
/* Useful for carousels that show a peek of the next item */
.carousel {
padding: 0 48px; /* Show 48px of adjacent items */
scroll-padding: 0 48px;
}Fix 5: Programmatic Scroll Management in SPAs
Single-page apps with client-side routing need explicit scroll management:
// React Router v6 — ScrollRestoration component
import { ScrollRestoration } from 'react-router-dom';
function App() {
return (
<>
<ScrollRestoration />
<Outlet />
</>
);
}
// Vue Router — scroll behavior
const router = createRouter({
history: createWebHistory(),
routes: [...],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
// Restore scroll position on browser back/forward
return savedPosition;
}
if (to.hash) {
// Smooth scroll to anchor
return {
el: to.hash,
behavior: 'smooth',
top: 80 // Account for fixed header
};
}
// Scroll to top on new navigation
return { top: 0, behavior: 'smooth' };
}
});
// Next.js App Router — scroll to top on route change
// Handled automatically; use scroll={false} on <Link> to disable
<Link href="/about" scroll={false}>About</Link>Fix 6: Advanced Scroll Snapping Patterns
Multi-item carousel (showing multiple items at once):
.carousel {
display: grid;
grid-auto-flow: column;
grid-auto-columns: calc(33.33% - 16px); /* 3 items visible */
gap: 24px;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-padding: 0 24px;
padding: 0 24px;
}
.carousel-item {
scroll-snap-align: start;
}Vertical scroll snap with variable-height sections:
/* Snap to sections even if they're taller than the viewport */
.page {
overflow-y: scroll;
scroll-snap-type: y proximity; /* proximity works better for tall sections */
}
.section {
/* Don't set min-height: 100vh — let content determine height */
scroll-snap-align: start;
scroll-margin-top: 64px;
}Detect snap position changes with JavaScript:
const carousel = document.querySelector('.carousel');
carousel.addEventListener('scrollsnapchanging', (e) => {
// Fires while user is dragging (Chrome 129+)
console.log('Snapping to:', e.snapTargetInline);
});
carousel.addEventListener('scrollsnapchange', (e) => {
// Fires when snap completes
const activeIndex = [...carousel.children].indexOf(e.snapTargetInline);
updateDots(activeIndex);
});Note:
scrollsnapchangeandscrollsnapchangingare available in Chrome 129+ and Safari 18.2+. For broader compatibility, use anIntersectionObserverto detect which item is in view.
Still Not Working?
scroll-behavior doesn’t work in some browsers — scroll-behavior is not supported in iOS Safari before version 15.4. For broader compatibility, use a JavaScript polyfill or pass behavior: 'smooth' explicitly in your scrollTo() calls.
Snap container must have a defined size — if overflow: scroll is on the snap container but the container has no explicit height (for vertical snap) or width (for horizontal snap), the container might not be scrollable at all. Verify with DevTools that the container is both scrollable and the correct element is triggering the scroll.
overscroll-behavior prevents scroll chaining — if scroll is stopping at the wrong container boundary (e.g., a modal capturing the page scroll), add overscroll-behavior: contain to the inner scroller:
.modal-body {
overflow-y: auto;
overscroll-behavior: contain; /* Don't propagate scroll to the page behind */
}Safari ignores scroll-snap-align on flex children — ensure each snap child has an explicit dimension. In flex containers, set flex: 0 0 auto plus a fixed width/height on each snap item to prevent Safari from treating them as zero-size.
For related CSS layout issues, see Fix: CSS Flexbox Not Working and Fix: CSS Position Sticky Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: CSS Grid Layout Not Working — Items Not Placing or Spanning Correctly
How to fix CSS Grid issues — implicit vs explicit grid, grid-template-areas, auto-placement, subgrid, alignment, and common mistakes with grid-column and grid-row.
Fix: Mantine Not Working — Styles Not Loading, Theme Not Applying, or Components Broken After Upgrade
How to fix Mantine UI issues — MantineProvider setup, PostCSS configuration, theme customization, dark mode, form validation with useForm, and Next.js App Router integration.
Fix: View Transitions API Not Working — No Animation Between Pages, Cross-Document Transitions Failing, or Fallback Missing
How to fix View Transitions API issues — same-document transitions, cross-document MPA transitions, view-transition-name CSS, Next.js and Astro integration, custom animations, and browser support.
Fix: Panda CSS Not Working — Styles Not Applying, Tokens Not Resolving, or Build Errors
How to fix Panda CSS issues — PostCSS setup, panda.config.ts token system, recipe and pattern definitions, conditional styles, responsive design, and integration with Next.js and Vite.