Scroll effects used to mean an event listener and a throttle function, and plenty of pages still ship that way. Progress bars, cards that fade in as you reach them, parallax layers and headers that stay pinned can all run now without a script reading the scroll position.

The native toolkit is scroll-behavior, scroll snap, sticky positioning, overscroll-behavior and scroll-driven animations. The last one is the recent addition. Chrome for Developers dates the change to Chrome 115 (July 2023), the first release where scroll-driven animations declared in CSS can run off the main thread.

What are CSS scroll effects?

See the Pen
CSS Scroll Reveal Sections
by Ryan Mulligan (@hexagoncircle)
on CodePen.

The label covers any front-end technique where CSS properties alone change an element’s position, appearance or snapping behavior as the page scrolls. No JavaScript scroll listeners involved.

Some of these change how a scroll container behaves: position: sticky, scroll snap, scroll-behavior and overscroll-behavior. The other group is scroll-driven animations, where animation-timeline ties ordinary keyframes to scroll position instead of time.

What falls outside is any effect where a script reads the scroll position on every event and moves things by hand. A scroll listener written in JavaScript can fake a sticky header or parallax just fine, it simply doesn’t count here.

The Firefox Source Docs point out that scroll-linked effects built on listeners run on the main thread, and that a lot of them can be rewritten in CSS so the compositor thread handles them instead.

So the rule of thumb is this. If the browser is driving the effect, it counts, and if your own script is, it doesn’t.

Which CSS properties create scroll effects?

See the Pen
Intersection Observer + CSS Vars Scroll Effect
by Emily Hayman (@eehayman)
on CodePen.

Four of them work without any animation at all: scroll-behavior, scroll-snap-type, position: sticky and overscroll-behavior. Everything animated goes through animation-timeline.

PropertyEffectJavaScript neededTypical use
scroll-behavior: smoothAnimated jumps to anchors and scripted scrollsNoIn-page navigation
scroll-snap-type, scroll-snap-alignScrolling stops at defined positionsNoCarousels, full-screen sections
position: stickyElement pins at an offsetNoHeaders, sidebars, table headings
overscroll-behaviorBlocks scroll chaining and bounceNoModals, chat panels
animation-timelineAnimation progress follows scrollNoProgress bars, reveals, parallax

Snapping and smooth scrolling

scroll-snap-type goes on the scroll container and takes an axis (x, y, block, inline or both) plus an optional strictness keyword, as MDN documents.

Is responsive design still a top priority?

Explore the latest responsive design statistics: adoption rates, performance impact, user behavior, and trends shaping modern websites.

See the Numbers →

Set it to mandatory and the container has to settle on a snap position whenever scrolling stops. With proximity, the browser makes that call itself, depending on where the scroll ended.

scroll-snap-align goes on the children and accepts start, end, center or none. Under mandatory snapping, MDN warns that a child with the value none (or left unset) can never be scrolled into view.

scroll-behavior: smooth animates anchor jumps and scripted scrolls. It won’t smooth out a mouse wheel, which catches a surprising number of people.

Sticky positioning

A sticky element behaves like a relative one until the scroll reaches its offset, then it pins.

It needs an offset such as top: 0, because without one nothing sticks, and a scroll container to pin inside, usually the page itself. It also needs room in its parent, since the element stops at the parent’s edge.

That last rule trips people up. Put a sticky navigation bar inside a short wrapper and it releases the moment the wrapper ends. I’ve seen this eat a whole afternoon.

Overscroll and scroll chaining

MDN marks the overscroll-behavior properties as Baseline Widely available, supported across browsers since September 2022.

Set overscroll-behavior: contain on a modal body or chat panel and reaching its end no longer scrolls the page behind it. The value none goes further and also blocks effects such as pull-to-refresh.

How do scroll-driven animations work?

Instead of a clock, the animation reads scroll position. You write ordinary keyframes, then point animation-timeline at a scroll progress timeline or a view progress timeline.

The browser maps scroll distance to animation progress, so there is no listener to throttle and no frame loop to run.

Scroll progress timeline

See the Pen
Scroll Drawing
by Chris Coyier (@chriscoyier)
on CodePen.

Progress here means how far a scroll container has scrolled along one axis, 0% at the start and 100% at the end (Chrome for Developers).

The scroll() function builds one inline, and it takes a scroller and an axis. The default scroller is nearest, the closest ancestor scroll container. Use root when you want the document viewport, or self if the element itself scrolls.

The axis is block, inline, x or y, so animation-timeline: scroll(root block) is the standard form for a page-level progress bar. Declare it after the animation shorthand, because the shorthand resets the timeline.

To share one timeline between several elements, name it with scroll-timeline-name and a dashed identifier such as –hero-scroll, then reference that name in animation-timeline.

View progress timeline

Where the scroll version watches the whole scroller, a view progress timeline watches a single element.

The view() function links progress to the element’s position inside its scroll container, so the animation starts as the element enters and finishes as it leaves.

Every list item can carry its own view() timeline. Staggered reveals then need no delay math in script, and view-timeline-name works the same way scroll-timeline-name does when elements need to share one.

Controlling start and end with animation-range

animation-range chooses which slice of the timeline the animation covers.

MDN’s list of timeline range names runs through cover, contain, entry, exit, entry-crossing and exit-crossing. The table below covers the four that come up most.

Range nameStarts whenEnds when
coverElement begins to enter the scrollportElement has fully left (the default)
containElement is fully contained by the scrollport, or fully covers itElement is no longer fully contained by or fully covering the scrollport
entryElement begins to enterElement has fully entered
exitElement begins to leaveElement has fully left

Percentages inset a range. A form MDN lists, animation-range: entry 10% exit 100%, starts 10% into the entry phase and runs to the end of the exit phase.

For an element taller than the scrollport, contain still has something to track. It runs from the moment the element first completely covers the scrollport to the moment it no longer does.

How to build common scroll effects with CSS

Most real pages get by with a reading progress bar, a reveal on scroll, parallax layers and a shrinking sticky header. Each one sticks to a single timeline type and needs only a few lines of CSS.

Reading progress bar

See the Pen
CSS only scroll indicator
by Mike (@MadeByMike)
on CodePen.

Point the timeline at scroll(root block) and animate transform from scaleX(0) to scaleX(1), with transform-origin set to the left edge. Fix the bar to the top of the page.

The bar is a plain element, so any progress bar styled with CSS works as the base. Animate transform, not width, because width forces layout on every frame.

Reveal on scroll

See the Pen
Knockout Text Scroll Reveal (Final)
by Blake Eric (@blakeeric)
on CodePen.

  1. Write keyframes that run from opacity 0 and translateY(24px) to opacity 1 and translateY(0).
  2. Apply them with animation: reveal linear both, where the both fill mode keeps the start and end states.
  3. Add animation-timeline: view() after the shorthand.
  4. Limit the slice with animation-range: entry, so the reveal finishes as the element finishes entering.
  5. Wrap the whole rule in @supports (animation-timeline: view()).

Step 5 gets skipped a lot. Without it, a browser that ignores the timeline applies only half the effect, and the animation plays once on the normal clock at page load instead of following the scroll. Inside @supports, unsupported browsers just show plain, static content.

Parallax layers

See the Pen
Parallax scrolling effect
by Oleksandr H. (@Kamilius)
on CodePen.

Layers move at different speeds against the same scroll. A named scroll timeline makes that easy, since every layer shares one source of progress and only the translate distance in its keyframes differs.

For the design side of parallax scrolling, restraint wins. It is the pattern most likely to feel like too much, so one or two layers usually beat five.

Sticky header and section scrubbing

See the Pen
Header Image Parallax Scrolling Effect with CSS
by WebMadeWell (@webmadewell)
on CodePen.

A shrinking header starts with position: sticky and top: 0, plus a scroll() timeline that scales the logo and fades a shadow in. Section scrubbing works differently. A tall container holds a sticky child, and the container’s scroll progress changes that child (an image swap, a text highlight).

Shrink with transform and opacity. Animating height or padding drags layout back onto the main thread.

When is CSS enough, and when does a JavaScript library still win?

CSS handles progress bars, reveals, parallax, sticky headers and snapping without help. A library still earns its place when you need fine-grained scroll callbacks, pinned multi-step sequences or a scrolling feel the browser does not provide.

Animations triggered on scroll with plain CSS now cover the bulk of what libraries were installed for.

OptionRuns onBest forLimit
Native CSS scroll-driven animationsCompositor thread for transform and opacityProgress bars, reveals, parallaxNo progress callbacks, uneven browser support
GSAP ScrollTriggerJavaScript, main threadPinning, sequences, callbacksScript weight
LenisJavaScript scroll layerCustom scrolling feelNo native scroll-snap support
AOSJavaScript plus CSS transitionsClass-based revealsReveals only

GSAP ScrollTrigger

GSAP’s README states that every plugin, ScrollTrigger included, is free for commercial use, a change the README credits to Webflow. Cost is no longer the argument against it.

What it does well:

  • Pinning and scrubbing across several timelines
  • Callbacks when a trigger is entered or left

It also works in every browser. The downsides are that it runs in JavaScript, so its work shares the main thread with the rest of the page, and that it adds a dependency to a project that may not need one.

Lenis

Lenis replaces the browser’s scrolling feel with a scripted one, which no CSS property does.

Its README is upfront about the price. CSS scroll-snap isn’t supported (you have to use lenis/snap instead), and smooth scrolling stops working over iframes, since iframes do not forward wheel events.

In its favor are custom scroll inertia plus horizontal and nested scroll. Against it, the snap workaround and the iframe gaps.

AOS

Data attributes drive AOS, a small JavaScript library that triggers CSS animations as elements enter the viewport.

Setup takes minutes, and it works without view timelines. But it only reveals. It can’t scrub an animation along with scroll distance, and view() replaces it wherever support allows.

My own order of operations: start with CSS, bring in GSAP only when you need fine-grained scroll callbacks or pinned sequences, and reach for Lenis only when the product genuinely needs its own scrolling feel and you accept the snap workaround.

Which browsers support CSS scroll effects?

Chrome, Edge and Safari run scroll-driven animations without a flag. Firefox has not shipped them enabled by default, and keeps them behind a preference.

Chrome for Developers puts Chrome and Edge at version 115. Safari shows up as version 26 in MDN’s compatibility data, introduced in the Safari 26 beta at WWDC25 (WebKit, June 2025).

Firefox is listed only as “preview” in MDN’s compatibility data, with no stable release shipping it enabled by default. The layout.css.scroll-driven-animations.enabled preference exists in about:config and is on by default in Nightly.

Scroll-driven animations were also named a focus area for Interop 2026, in an announcement dated February 12, 2026 (web.dev).

One developer write-up places stable Firefox 152 (June 2026) in the same flagged state. That is the most likely entry to have changed since, so check MDN’s table before you ship.

Not every source agrees, though. A 2026 developer blog says Safari added support in 18.2 and Firefox shipped it in 133. MDN’s compatibility data and WebKit’s own WWDC25 material contradict both claims.

Trust the primary data, then test anyway.

Firefox is the practical gap, so plan for cross-browser compatibility from the first line of CSS. Build each effect as an enhancement and test it in the browser you least expect to break.

How to ship scroll effects in browsers that lack support

Fallbacks stack. A feature test comes first, a polyfill after that, and an Intersection Observer reveal as the last option. Most pages never get past the first.

The feature test should check the exact value you use, such as @supports (animation-timeline: view()). A script can ask the same question with CSS.supports(‘animation-timeline’, ‘scroll()’) before it loads anything heavier.

The scroll-timeline polyfill on GitHub (flackr/scroll-timeline) implements ScrollTimeline and ViewTimeline, and it reads CSS animations that use scroll-timeline or view-timeline.

Its README does flag a limit. Stylesheets loaded from other origins may not be fetched and applied, because of browser security restrictions. And since it is JavaScript, the work lands back on the main thread.

Intersection Observer works differently. The API reports when a target crosses a threshold inside a root, and MDN notes the callback runs on the main thread. The default threshold is 0, so the callback fires as soon as a single pixel enters the viewport.

It reports entering and leaving, not progress. That makes it fine for a class toggle plus a CSS transition, and useless for scrubbing.

If the content only has to stay visible, the feature test alone is enough. Firefox users who need the same scrubbed effect call for the polyfill. And when an enter or leave toggle will do, Intersection Observer with a CSS transition is the lighter choice.

Start with the feature test. Add the others only when Firefox traffic justifies the extra script.

Do CSS scroll effects stay smooth?

They stay smooth when the animation touches only compositor-friendly properties. transform, opacity and filter skip layout and paint, so the compositor thread can run them even while the main thread is busy.

Stick to transform (translate, scale, rotate), opacity and filter. Width and height, margin and padding, and top and left all stay bound to the main thread, so avoid animating them.

Lighthouse defines a non-composited animation as any animation that triggers the Style, Layout or Paint step of the rendering pipeline. Those animations can look janky on low-end phones or while heavy tasks occupy the main thread (Chrome for Developers).

The same documentation adds a second cost. Non-composited animations that move elements can raise Cumulative Layout Shift, while composited animations are excluded from it.

The Avoid non-composited animations audit lists every offending DOM node with the failure reasons Chrome reports. Run it after each new effect. A header that shrinks by animating height shows up immediately.

How to make scroll effects accessible

Wrap scroll-driven motion in @media (prefers-reduced-motion: no-preference), so only users who have not asked for less motion receive it. That media query reads the operating system setting, and MDN names scaling and panning large objects as common vestibular triggers.

Each effect carries a different risk, so handle them separately. Parallax should go entirely under reduce. Reveals and slides can swap translate and scale for a plain opacity fade. Progress bars can stay, since they convey position, and smooth scrolling should be reset to scroll-behavior: auto.

WCAG 2.2 still carries Success Criterion 2.3.3, Animation from Interactions. It sits at Level AAA, arrived in WCAG 2.1, and says motion animation triggered by interaction must be disableable unless the motion carries the function or the information.

W3C’s Understanding document singles out parallax scrolling as often non-essential. Reduced motion is one slice of web accessibility, but it is the slice scroll effects touch most.

Guidance isn’t uniform here either. MDN’s example tones a scaling animation down to a dissolve under reduce, while stricter guidance removes motion entirely. A workable split is fades for movement and removal for parallax.

Fallbacks need the same check. A polyfill or Intersection Observer reveal reads the setting with window.matchMedia(‘(prefers-reduced-motion: reduce)’).

Keyboard users need one more fix. A sticky header covers whatever receives focus unless the scroll container sets scroll-padding-top to the header height.

To test, emulate prefers-reduced-motion from the Rendering tools in Chrome DevTools, or switch Reduce Motion on in the operating system.

When do CSS scroll effects break or backfire?

Most failures are silent. The CSS parses, nothing throws, and the effect simply does not appear.

A sticky element that never pins usually has an ancestor with overflow set to hidden, auto or scroll. That ancestor becomes the scrolling context, so the element pins to it instead of the page. Remove the overflow, switch it to overflow: clip, or make that ancestor the scroller.

A progress bar that stays frozen is normally a scroll() problem. It resolves to the nearest ancestor scroll container, which can be a wrapper with overflow: auto rather than the page, and a container with nothing to scroll gives the timeline no progress. Use scroll(root) or name the timeline on the right element.

Mandatory snapping can feel like a trap. Full-page snapping turns browsing into a slide deck and frustrates anyone reading part of a section. Unless the layout really is a deck, proximity is the better default for usability.

Scroll-driven animations versus listener-based effects

The upsides are mostly about what you no longer have to maintain:

  • No listener to throttle or clean up
  • Compositor-thread execution for transform, opacity and filter
  • Declarative rules that read like any other CSS animation
  • Static content remains if the browser ignores the timeline

On the downside:

  • Support is uneven, and Firefox is the open question
  • It handles animation only, with no per-progress logic
  • Range syntax takes practice before it feels natural

CSS Scroll Effects Examples To Check Out

Enter the World of Full Page Parallax

See the Pen
Full Page Parallax Scroll Effect
by Emily Hayman (@eehayman)
on CodePen.

From Dawn till Dusk: Parallax Scroll Animation

See the Pen
Parallax scroll animation
by isladjan (@isladjan)
on CodePen.

Back to Basics: Parallax Scrolling Background

See the Pen
Parallax Scrolling Background Image Effect
by Amr SubZero (@AmrSubZero)
on CodePen.

Starry Night Vibes: Mountain Parallax

See the Pen
Night on the mountain
by Mikael Ainalem (@ainalem)
on CodePen.

Deep Dive: Perspective Scrolling in CSS

See the Pen
Perspective Scrolling in CSS
by Hornebom (@Hornebom)
on CodePen.

Tilted Views: Skew Scrolling Effect

See the Pen
Skew Scrolling Effect ▲
by Dronca Raul (@rauldronca)
on CodePen.

In the Spotlight: CSS Fixed Conic Fill

See the Pen
CSS Fixed Conic Fill
by Adam Argyle (@argyleink)
on CodePen.

Highlight the Way: CSS Scroll Action

See the Pen
highlight on scroll with css-variables
by Martin Schuhfuss (@usefulthink)
on CodePen.

Round and Round: Scrolling in a Window

See the Pen
Scrolling Text Window
by Andretti Brown (@andrettibrown)
on CodePen.

Selector’s Choice: 12 nth

See the Pen
12 nth Selectors
by Gabriele Corti (@borntofrappe)
on CodePen.

Page Scroll Goodies

See the Pen
Page Scroll Effects
by Steve (@slstudios)
on CodePen.

Vertical Wordplay

See the Pen
Vertical scrolling text
by Azri Kahar (@azrikahar)
on CodePen.

The Scroll Show

See the Pen
Scroll Based Animation
by lmgonzalves (@lmgonzalves)
on CodePen.

Floatin’ Bubbles

See the Pen
Floaty Bubbles
by Will Boyd (@lonekorean)
on CodePen.

Peek-a-boo! Animation Time!

See the Pen
scroll animating effect
by Dima (@dimaZubkov)
on CodePen.

Chameleon Backgrounds

See the Pen
CSS background change on scroll
by Giana (@giana)
on CodePen.

Keep Rollin’, Rollin’, Rollin’

See the Pen
Horizontal Infinite “Out of Synch” Scroll Effect
by Julien Lejeune (@jlnljn)
on CodePen.

Crafty Paper Scrollin’

See the Pen
The Scroll of Lorem Ipsum
by Ben Rowles (@browles)
on CodePen.

One Stop Scroll Shop

See the Pen
[Pure CSS] – One page scroll
by Quentin Veron (@VeronQ)
on CodePen.

Seeing Double with Dual Color

See the Pen
Dual Color Text Scroll Effect
by Raymond Lopez (@raylopro)
on CodePen.

Now You See Me, Now You Don’t

See the Pen
Fade on Scroll
by foleyatwork (@foleyatwork)
on CodePen.

One Page Roller Coaster

See the Pen
One page scroll navigation with css transforms
by Nikolay Talanov (@suez)
on CodePen.

Scrolling Through Breaking News

See the Pen
Breaking News Scrolling Text Modal
by KB (@notkieran)
on CodePen.

Skewed but So Right

See the Pen
Skewed One Page Scroll
by Nikolay Talanov (@suez)
on CodePen.

Coloring Outside the Lines

See the Pen
Expanding Diagonal Background
by Derek Palladino (@derekjp)
on CodePen.

Slide, Drag, Flip, and Hop!

See the Pen
Horizontal scroll, drag, transition, bounce
by jesper landberg (@ReGGae)
on CodePen.

Keep It Simple, Make It Sassy!

See the Pen
simple scroll animation
by Mert Cukuren (@knyttneve)
on CodePen.

Box It Up and Scroll It Down

See the Pen
Box Scroll Effect
by Vivek (@aPenHasNoName)
on CodePen.

Hidden Treasures and Scroll Delights

See the Pen
Hidden & scrolling text
by Avaz Bokiev (@samarkandiy)
on CodePen.

ScrollPane Extravaganza

See the Pen
ScrollPane
by mainserv (@mainserv)
on CodePen.

Subtle, Smooth, and Sooo On Point

See the Pen
CSS Animations on Scroll – Fade From Bottom up
by Rxn (@iron-gem)
on CodePen.

CSS Scroll Effects FAQ

What is the difference between scroll-padding and scroll-margin?

scroll-padding lives on the scroll container and shrinks the area where snapped or linked content lands. The target element is where scroll-margin goes, and it pushes that element’s snap edge outward.

For a fixed header, set scroll-padding-top once on the container instead of margins on every target.

Can CSS fire a callback when an effect finishes?

Only partly. A CSS animation driven by a scroll or view timeline dispatches the usual animationstart and animationend events, and the Web Animations API exposes finish events for the same animations. CSS itself has no callback for reaching a particular point or percentage of progress inside a timeline.

Logic that must run on completion or visibility, such as loading data or tracking whether an element is visible, still usually needs JavaScript, typically an Intersection Observer.

Can JavaScript drive the same scroll timelines?

Yes. The Web Animations API exposes ScrollTimeline and ViewTimeline, and either one can be passed as the timeline option of element.animate().

That suits effects whose keyframes are computed at runtime, while static effects stay simpler in CSS.

Is horizontal scrolling possible with CSS only?

See the Pen
Horizontal scroller edge fade mask
by Adam Argyle (@argyleink)
on CodePen.

Yes. Set overflow-x: auto on the container, add scroll-snap-type: x with scroll-snap-align on the children, and you have a carousel without a script.

To animate along it, use scroll(nearest inline) as the timeline.

Why does my scroll animation not play in Firefox?

Stable Firefox does not enable scroll-driven animations by default. They sit behind the layout.css.scroll-driven-animations.enabled preference, so the animation-timeline declaration is ignored for most visitors.

Enable the preference in about:config only for testing, and protect visitors with an @supports block.

Where to Start With CSS Scroll Effects

Build a reading progress bar first, then a view-timeline reveal, and add parallax last. That order runs from the effect that carries information to the one that carries the most motion.

  1. Progress bar on scroll(root block)
  2. Reveal on view() with animation-range: entry
  3. Parallax layers, removed under reduced motion

To verify, run Chrome, Safari 26 and stable Firefox with the flag off, and emulate prefers-reduced-motion in each. The default Firefox run matters most, because it shows the page that unflagged visitors receive.

The trade-off is that Firefox visitors get the static page, not the effect. That changes once Firefox ships scroll-driven animations unflagged, and the MDN compatibility data checked in October 2026 still lists Firefox only as “preview”.

The progress bar is the quickest first build, and a guide to a CSS progress bar covers the element’s styling before any timeline is attached.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)