Skip to content

Fix: CSS Scroll Behavior Not Working — smooth scroll, scroll-snap, or scroll-margin Ignored

FixDevs ·

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 setting

Why 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: smooth is intentionally overridden.
  • The scrolling container must have overflowscroll-snap-type only works on the element that is actually scrolling. If the scroll happens on <html> or a parent, setting scroll-snap-type on a child has no effect.
  • scroll-snap-align requires defined dimensions — snap items must have explicit width/height (or flex/grid sizing) 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 the id element to the top of the viewport, placing it directly behind a fixed header.
  • JavaScript scrollTo vs CSS scroll-behaviorscroll-behavior in CSS affects scrolling triggered by anchor navigation and scrollTo() with behavior: 'smooth'. Without passing behavior: 'smooth' to scrollTo(), 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.

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: scrollsnapchange and scrollsnapchanging are available in Chrome 129+ and Safari 18.2+. For broader compatibility, use an IntersectionObserver to detect which item is in view.

Still Not Working?

scroll-behavior doesn’t work in some browsersscroll-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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles