Back to blog
Web Development

How to Make Smooth Scroll Animations on a Website Using CSS

Create smooth scroll animations with pure CSS: smooth anchor scrolling, scroll snapping, and scroll-driven reveal effects that respect reduced-motion settings.

AdminOctober 19, 202610 min read2 views
How to Make Smooth Scroll Animations on a Website Using CSS

How to Make Smooth Scroll Animations on a Website Using CSS

Scroll animations used to mean heavy JavaScript libraries listening to every scroll event and recalculating positions dozens of times per second. Modern CSS can now handle most of it natively. Smooth scroll animations are visual effects tied to scrolling, such as smooth jumps to page sections, elements fading in as they enter the viewport, and progress indicators that fill as the reader moves down the page.

Quick Answer: Make smooth scroll animations with CSS by adding scroll-behavior: smooth for anchor links, using scroll-margin-top to offset sticky headers, and creating reveal effects with scroll-driven animations using animation-timeline: view(). Animate only transform and opacity, wrap enhancements in @supports, and disable motion for users who set prefers-reduced-motion.

What Are CSS Scroll Animations and How Do They Work?

CSS scroll animations fall into three distinct categories, and confusing them is the most common source of frustration. Each solves a different problem with different properties.

Smooth scrolling changes how the browser travels to a destination. Setting scroll-behavior: smooth on the html element makes in-page anchor links glide to their target instead of jumping instantly. It requires a single declaration and no animation keyframes.

Scroll snapping controls where scrolling comes to rest. With scroll-snap-type on a container and scroll-snap-align on children, carousels and full-screen sections settle neatly into place. Because it works on any scroll container, it pairs well with responsive layouts in pure CSS, such as horizontally scrolling card rows on phones.

Scroll-driven animations link a keyframe animation's progress to scroll position instead of time. The animation-timeline property accepts scroll(), which tracks a scroll container's progress, or view(), which tracks an element's progress through the visible area. The animation-range property defines which portion of that journey the animation covers, such as entry 0% cover 40%.

The performance benefit is significant. Because scroll-driven animations run declaratively, the browser can often run them off the main thread when they animate compositor-friendly properties like transform and opacity. That avoids the jank common with scroll event listeners that read layout on every frame.

Step-by-Step: Adding Scroll Animations With CSS

These steps build up from the simplest technique to the most advanced, and each works independently. Apply them in order and stop wherever your design needs end.

  1. Enable smooth anchor scrolling. Add html { scroll-behavior: smooth; }. Every link pointing to #section-id now glides to its target.
  2. Offset sticky headers. Add scroll-margin-top to target sections, or scroll-padding-top on html, equal to your header height. Otherwise the header covers each heading you scroll to.
  3. Respect reduced motion immediately. Wrap the smooth behavior so it only applies inside @media (prefers-reduced-motion: no-preference). Users who request less motion get instant jumps.
  4. Define a reveal keyframe. Write @keyframes reveal { from { opacity: 0; transform: translateY(2rem); } to { opacity: 1; transform: none; } }. Only transform and opacity change, keeping the effect cheap to render.
  5. Attach it to a view timeline. On elements with a .reveal class, set animation: reveal linear both; animation-timeline: view(); animation-range: entry 0% cover 30%;. The element fades and rises as it enters, completing when it is partly on screen.
  6. Guard with feature detection. Wrap step 5 in @supports (animation-timeline: view()) so browsers without support simply show content normally with no hidden elements.
  7. Add a reading progress bar. Create a fixed bar with transform-origin: left, a keyframe from scaleX(0) to scaleX(1), and animation-timeline: scroll(root). It fills as the page scrolls with no JavaScript.
  8. Add scroll snapping where useful. For a horizontal gallery, use scroll-snap-type: x mandatory on the container and scroll-snap-align: start on each item, plus overflow-x: auto.
  9. Test in multiple browsers. Check the experience with and without scroll-driven animation support, and with reduced motion enabled in your operating system settings.

Note the order of steps 3 and 6. Accessibility and graceful fallback come before polish, so the page is complete and usable in every browser even if the advanced effects never run.

Comparing CSS Scroll Techniques

Each technique has a specific purpose and a distinct browser support profile. Verify current support on Can I Use before relying on any of them for critical content, since scroll-driven animations in particular are still rolling out across engines.

TechniqueKey PropertiesTypical UseFallback Behavior
Smooth scrollingscroll-behaviorAnchor links and table of contentsInstant jump to the target
Scroll offsetscroll-margin-top, scroll-padding-topSticky header compensationTarget may hide under the header
Scroll snappingscroll-snap-type, scroll-snap-alignCarousels, galleries, slide sectionsFree scrolling without snap points
View timeline animationanimation-timeline: view(), animation-rangeReveal effects as elements enterContent appears without animation when guarded
Scroll timeline animationanimation-timeline: scroll()Progress bars and parallax-style effectsStatic element with no progress effect

The fallback column is the reason CSS scroll animations are low-risk. When written with @supports guards, unsupported browsers receive the plain, fully readable page, which is exactly how progressive enhancement should work.

Motion, Performance, and Accessibility: Practitioner Analysis

Animation can guide attention or become a barrier, and the difference is mostly restraint. The points below combine WCAG guidance with lessons from production projects.

Motion can make people unwell. Users with vestibular disorders can experience dizziness or nausea from large movements, parallax, and zooming effects. Operating systems offer a reduce-motion setting, which browsers expose through prefers-reduced-motion: reduce. WCAG 2.3.3, Animation from Interactions, at level AAA, recommends that motion triggered by interaction can be disabled, and WCAG 2.2.2 at level A requires a way to pause moving content that starts automatically and lasts more than five seconds. The guidance on accessibility guidelines for motion explains how to test these settings across devices.

Animate cheap properties. Animating width, height, top, or margin forces layout recalculation on every frame. Animating transform and opacity can usually be handled by the compositor, which keeps scrolling smooth on lower-powered phones.

Never hide content behind an animation. If elements start at opacity: 0 outside a supports guard, users in unsupported browsers, or with failed scripts in JavaScript-based approaches, see a blank page. Initial hidden states must only exist where the animation that reveals them is guaranteed to run.

Subtle beats dramatic. In my experience, short travel distances of one to three rem with quick ranges feel polished, while large slides and rotations feel gimmicky by the third section. Animate a few meaningful elements, not every paragraph.

Common Scroll Animation Mistakes to Avoid

These mistakes appear regularly in audits and code reviews. Most are fixed with a single property or media query.

  • Ignoring prefers-reduced-motion. Shipping motion with no opt-out can cause physical discomfort for some users. Add a reduced-motion query that disables reveals, parallax, and smooth scrolling.
  • Scroll-jacking. Overriding natural scroll speed or direction breaks user expectations and trackpad behavior. Let the browser own scrolling, and animate elements in response to it.
  • Forgetting sticky header offsets. Smooth scrolling to a heading that ends up under a sticky header looks broken. Use scroll-margin-top on targets.
  • Mandatory snapping on long content. scroll-snap-type: y mandatory on sections taller than the viewport can trap users, making parts of content unreachable. Use proximity or limit snapping to fixed-height items.
  • Animating layout properties. Height and margin animations cause jank on mid-range devices. Use transforms instead.
  • Using a heavy library for simple reveals. Pulling in a large JavaScript dependency for fade-ins adds download and execution cost. Use CSS scroll-driven animations with a guard, or a small Intersection Observer script as a fallback.

Walkthrough: A Product Landing Page With Scroll Effects

Here is a realistic application: a product landing page with a sticky header, a table of contents, feature sections, a testimonial carousel, and a reading progress bar.

Navigation. The header links to #features, #pricing, and #faq. With smooth scrolling enabled under a no-preference motion query and scroll-padding-top: 5rem on the root, each link glides to a heading that sits just below the sticky header.

Feature reveals. Each feature card has the .reveal class. Inside @supports (animation-timeline: view()) and the no-preference motion query, cards fade up by two rem as they enter. The range entry 10% cover 30% finishes the effect early so text is fully readable before users focus on it.

Staggering without JavaScript. Cards in the same row use slightly different ranges, such as adding five percent to the start of each subsequent card, producing a natural cascade tied to scroll rather than time.

Testimonial carousel. A horizontal container with scroll-snap-type: x mandatory, cards at about eighty-five percent width on phones so the next card peeks into view, signaling that the row scrolls. Previous and next buttons scroll the container programmatically for keyboard and mouse users.

Progress bar. A thin bar at the top of the viewport uses animation-timeline: scroll(root) to scale from zero to full width. Under reduced motion it remains visible but simply updates without animated easing, since it conveys position rather than decoration.

Verification. Test in a Chromium-based browser, Safari, and Firefox to confirm effects run where supported and content remains fully visible where they do not. Enable reduced motion in system settings and reload. Tab through the page to ensure focus moves logically and the carousel buttons work.

The entire effect set adds well under a hundred lines of CSS and a few lines of JavaScript for carousel buttons, with no animation library and no scroll event listeners.

Key Takeaways

  • CSS separates scroll effects into smooth scrolling, scroll snapping, and scroll-driven animations, each with its own properties.
  • Scroll-driven animations using animation-timeline: view() can create reveal effects without any scroll event listeners.
  • Animating only transform and opacity keeps scroll effects smooth because they avoid layout recalculation.
  • Wrapping advanced effects in @supports ensures unsupported browsers still display all content normally.
  • Every scroll animation should be disabled or reduced when the user's system requests reduced motion.

Frequently Asked Questions

How do I enable smooth scrolling with CSS?

Add scroll-behavior: smooth to the html element. Anchor links that point to an id on the same page will then glide to the target instead of jumping. Wrap the declaration in a prefers-reduced-motion: no-preference media query so users who request reduced motion still get instant navigation.

Can I create scroll-triggered animations without JavaScript?

Yes, in browsers that support scroll-driven animations. Set animation-timeline: view() on an element with a keyframe animation, and its progress follows the element's position in the viewport. Guard the code with @supports so other browsers simply show content without animation. A small Intersection Observer script can serve as a fallback.

Which browsers support CSS scroll-driven animations?

Chromium-based browsers such as Chrome and Edge have supported scroll-driven animations since version 115, and support has been expanding to other engines. Because support changes over time, check Can I Use for current status and always wrap these effects in @supports so unsupported browsers still display all content.

Why does smooth scrolling hide my section headings?

A sticky or fixed header overlaps the top of the viewport, and the browser scrolls the target to the very top by default. Add scroll-padding-top to the html element, or scroll-margin-top to target sections, with a value equal to the header height so headings land just beneath it.

Do scroll animations hurt website performance?

They can if they animate layout properties or rely on scroll event listeners that read layout on every frame. CSS scroll-driven animations that change only transform and opacity are typically efficient because the browser can optimize them. Keep animated elements few and test on a mid-range phone.

How do I make scroll animations accessible?

Respect the prefers-reduced-motion setting by disabling or simplifying animations when it is set to reduce. Never hide content in a way that depends on an animation running. Keep movement subtle, avoid scroll-jacking and large parallax effects, and provide controls to pause any motion that plays automatically.

Conclusion

The most important principle is that scroll animation should enhance a page that already works perfectly without it. Start with smooth anchor scrolling and header offsets, add guarded scroll-driven reveals sparingly, and honor reduced motion everywhere. Your next step is to add the reveal pattern to one section of your site, then extend your preference-aware styling with dark mode with CSS and JavaScript, which follows the same media query approach.

Chat on WhatsApp