GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Animation on Scroll Effects, No JavaScript

A css animation on scroll ties motion to how far the page has scrolled, using CSS's own animation-timeline instead of a scroll listener.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order follows how a page reveals itself as you scroll. 01 through 04 fire once, the moment an element enters the viewport. 05 through 07 read your position across the whole document rather than a single element. 08 and 09 combine both — a single trigger, timed to the surrounding scroll. Watch them loop in the grid above — every section below keeps only the essential lines, and the untrimmed files live in the zip. When one parallax entry like 07 isn't enough depth, the CSS parallax scrolling set is that single item stretched across nine, and if 03's wipe leaves you wanting to move the sentence itself, the CSS text animation effects take it from there.

01Fade up

The motion

Rises slightly and fades in the moment it enters the viewport, using animation-timeline: view() instead of an IntersectionObserver.

Fade up
Open in a new tab

01 · Fade up — Measured area 38.606% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

24px멀리 48px페이드만

Where a css scroll reveal animation belongs

It's the default entrance for section titles, cards, and paragraphs — almost anything that needs to announce itself once and get out of the way.

.card {
  animation: fade-up $easing both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}
@keyframes fade-up {
  from { opacity: 0; transform: translateY($distance); }
  to   { opacity: 1; transform: translateY(0); }
}

02Staggered list

What it does

List items appear one after another as you scroll past them, roughly 80ms apart, without a single setTimeout.

Staggered list
Open in a new tab

02 · Staggered list — Measured area 34.529% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

80ms빠르게 40ms역순

When to use it

The stagger comes free from the list's own layout, since each item watches its own entry into the viewport. Use it for feature lists, pricing tables, and team grids.

.list__item {
  animation: stagger-in $ease-out both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}
@keyframes stagger-in {
  from { opacity: 0; transform: translateY($distance); }
  to   { opacity: 1; transform: translateY(0); }
}

03Text wipe

What you see

A headline wipes in from the left, revealed by a single animated clip-path instead of a moving width or left offset.

Text wipe
Open in a new tab

03 · Text wipe — Measured area 2.99% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

600ms위에서가운데서

Where it fits

Use it for hero headlines and section titles that need one confident entrance.

.headline {
  animation: wipe-in $easing both;
  animation-timeline: view();
  animation-range: entry 20% cover 60%;
}
@keyframes wipe-in {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

04Image curtain

The motion

A colored panel covers the image, then slides off to the right as the frame scrolls into view.

Image curtain
Open in a new tab

04 · Image curtain — Measured area 40.756% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

주황검정위로

Good places for it

It suits portfolios and product photos, where the reveal itself is part of the design.

.frame__curtain {
  transform-origin: right;
  animation: curtain-open $easing both;
  animation-timeline: view();
  animation-range: entry 10% cover 50%;
}
@keyframes curtain-open {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

05Reading progress

What it does

A bar fixed to the top of the page fills as you read, driven only by a scroll() timeline tied to the document instead of a scroll listener that recalculates on every frame.

Reading progress
Open in a new tab

05 · Reading progress — Measured area 1.0% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

3px굵게 6px하단

When to use it

It suits long articles and documentation, where readers want to see how much is left.

.bar {
  position: fixed;
  top: 0;
  left: 0;
  transform-origin: left;
  animation: fill-bar linear both;
  animation-timeline: scroll(root);
}
@keyframes fill-bar {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

06Shrinking header

What you see

The sticky header thins out and its logo scales down as the page scrolls, using transform: scaleY instead of animating height.

Shrinking header
Open in a new tab

06 · Shrinking header — Measured area 7.342% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

64→48px로고만그림자 추가

Where it fits

Use it on landing pages and blog headers that need to stay pinned without eating the whole screen.

.head {
  position: sticky;
  top: 0;
  transform-origin: top;
  animation: shrink-header linear both;
  animation-timeline: scroll(root);
  animation-range: 0 200px;
}
@keyframes shrink-header {
  from { transform: scaleY(1); }
  to   { transform: scaleY(.75); }
}

07Parallax layers

The motion

The background drifts slower than the foreground content as the page scrolls, and the gap between the two speeds is what reads as depth.

Parallax layers
Open in a new tab

07 · Parallax layers — Measured area 13.925% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

0.5배0.3배두 겹

Good places for it

Use it for hero backgrounds and the breaks between sections.

.layer--back {
  animation: drift-back linear both;
  animation-timeline: scroll(root);
}
.layer--front {
  animation: drift-front linear both;
  animation-timeline: scroll(root);
}
@keyframes drift-back {
  from { transform: translateY(0); }
  to   { transform: translateY(-20px); }
}
@keyframes drift-front {
  from { transform: translateY(0); }
  to   { transform: translateY(-40px); }
}

08Count up

What it does

The number counts from 0 to its target the instant it's visible, using @property to register the custom value as an integer so the browser interpolates whole numbers instead of decimals.

Count up
Open in a new tab

08 · Count up — Measured area 3.302% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

1.2s천 단위 콤마% 붙이기

When to use it

No JavaScript reads the value; the CSS counter does. Use it for stats sections, anywhere a static number would look inert.

@property --num {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
.stat__num {
  --num: 0;
  counter-reset: num var(--num);
  animation: count-up 1.2s linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
  &::after { content: counter(num); }
}
@keyframes count-up {
  from { --num: 0; }
  to   { --num: 1280; }
}

09Line-draw timeline

What you see

A vertical line draws itself as you scroll the page, and each dot lights up once the line has passed it.

Line-draw timeline
Open in a new tab

09 · Line-draw timeline — Measured area 0.553% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

2px가로형점 없이

Where it fits

Use it for company history, step-by-step guides, and roadmaps, where progress itself is the point.

.timeline__line {
  transform-origin: top;
  animation: line-draw linear both;
  animation-timeline: scroll(root);
  animation-range: 0 200px;
}
.timeline__dot {
  animation: dot-light $duration ease-out both;
  animation-timeline: view();
  animation-range: entry 30% cover 60%;
}
@keyframes line-draw {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}
@keyframes dot-light {
  from { opacity: .28; }
  to   { opacity: 1; }
}

Where it breaks — the traps

Why animation-timeline, not a scroll listener

All nine use CSS's native animation-timeline instead of a scroll event listener, so nothing runs in JavaScript on every scroll frame, as MDN's guide to scroll-driven animations describes. The first trap is browser support: view() and scroll() timelines are newer than most of CSS, so every file wraps the moving rule in @supports (animation-timeline: view()) and a matching @supports not block. The fallback shows the finished state, which is the difference between a demo that degrades gracefully and one that just stays invisible forever in an older browser. Where the trigger is opening and closing rather than scrolling, that same fallback duty shifts onto @starting-style, and the CSS modal animation patterns work through what it does and doesn't cover.

Trap two — a pixel budget

The second trap is 06's animation-range: 0 200px. That's a fixed pixel budget, not a percentage of the page, so the header only finishes shrinking once you've scrolled past 200px total, and setting it larger than the page's own scrollable distance leaves the header stuck halfway. Those numbers are already tuned against the actual demo pages, and the zip's password is x5xz5uh4 if you'd rather stop reading and open it now.

Tying a number to scroll the way 08 does collects all three of these traps at once, which is why counting up on its own timing — no scroll involved — is gathered separately in the CSS number counter animations.

Trap three — the preview frame

The third trap is the preview above: the grid sits inside a fixed 480×300 iframe, which is too short to scroll for real, so every demo falls back to a two-second .is-demo loop instead of the genuine scroll-timeline. To see 05's bar or 09's line actually respond to your scroll position, open the vanilla file directly and scroll the page yourself; the loop is a stand-in, not the real behavior.

Accessibility

Reduced motion

Every demo answers prefers-reduced-motion, but the fallback isn't always "turn it off" — sometimes it's "skip straight to the result." 01 through 04 drop their animation and show the finished state immediately: opacity 1, the wipe fully open, the curtain already pulled back. 08 sets --num straight to its target number instead of counting up, and 09 draws the line and lights every dot without the sweep. 06 and 07 lock into their end state too, since a permanently shrunk header and still parallax layers aren't decorative enough to keep once motion is off. MDN's guide to prefers-reduced-motion covers browser support for the query.

@media (prefers-reduced-motion: reduce) {
  .card, .list__item, .headline, .frame__curtain { animation: none !important; }
  .head, .timeline__line { animation: none !important; transform: scaleY(1); }
  .stat__num { animation: none !important; --num: 1280; }
}

More scroll animation css examples live in the CSS category, and what this site actually is gets explained on the about page.

FAQ

Is this really a css animation on scroll without javascript?

No. Every animation here runs on animation-timeline: view() or scroll(), which is a CSS property, not a scroll listener. The React folder in the zip only wraps the same SCSS in components; it doesn't drive any of the actual motion with JS.

What's the difference between view() and scroll()?

view() times an animation to one element's own trip through the viewport, so 01 through 04, 08, and the dots in 09 use it. scroll() instead reads how far the whole document has moved, which is what drives 05, 06, 07, and the line in 09. Pick view() when you're animating an element into place, and scroll() when the motion should track total scroll progress.

Can I use these with Tailwind or a React build?

Yes. Tailwind's utility classes and this SCSS don't conflict, since the motion lives entirely in animation-timeline and keyframes rather than in classes Tailwind would also want to control. The zip's react/ folder has one component per effect that takes the same variables as props, so nothing needs rewriting to fit a component library.

Enter the archive password

The password is inside this article. You will find it as you read.