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.
| Property | Effect | JavaScript needed | Typical use |
|---|---|---|---|
| scroll-behavior: smooth | Animated jumps to anchors and scripted scrolls | No | In-page navigation |
| scroll-snap-type, scroll-snap-align | Scrolling stops at defined positions | No | Carousels, full-screen sections |
| position: sticky | Element pins at an offset | No | Headers, sidebars, table headings |
| overscroll-behavior | Blocks scroll chaining and bounce | No | Modals, chat panels |
| animation-timeline | Animation progress follows scroll | No | Progress 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.
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 name | Starts when | Ends when |
|---|---|---|
| cover | Element begins to enter the scrollport | Element has fully left (the default) |
| contain | Element is fully contained by the scrollport, or fully covers it | Element is no longer fully contained by or fully covering the scrollport |
| entry | Element begins to enter | Element has fully entered |
| exit | Element begins to leave | Element 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.
- Write keyframes that run from opacity 0 and translateY(24px) to opacity 1 and translateY(0).
- Apply them with animation: reveal linear both, where the both fill mode keeps the start and end states.
- Add animation-timeline: view() after the shorthand.
- Limit the slice with animation-range: entry, so the reveal finishes as the element finishes entering.
- 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.
| Option | Runs on | Best for | Limit |
|---|---|---|---|
| Native CSS scroll-driven animations | Compositor thread for transform and opacity | Progress bars, reveals, parallax | No progress callbacks, uneven browser support |
| GSAP ScrollTrigger | JavaScript, main thread | Pinning, sequences, callbacks | Script weight |
| Lenis | JavaScript scroll layer | Custom scrolling feel | No native scroll-snap support |
| AOS | JavaScript plus CSS transitions | Class-based reveals | Reveals 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.
- Progress bar on scroll(root block)
- Reveal on view() with animation-range: entry
- 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.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


