GODRICH

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.

01Fade up

Rises slightly and fades in the moment it enters the viewport, using animation-timeline: view() instead of an IntersectionObserver. 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.

24px멀리 48px페이드만
.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

List items appear one after another as you scroll past them, roughly 80ms apart, without a single setTimeout. 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.

80ms빠르게 40ms역순
.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

A headline wipes in from the left, revealed by a single animated clip-path instead of a moving width or left offset. Use it for hero headlines and section titles that need one confident entrance.

600ms위에서가운데서
.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

A colored panel covers the image, then slides off to the right as the frame scrolls into view. 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

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. It suits long articles and documentation, where readers want to see how much is left.

3px굵게 6px하단
.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

The sticky header thins out and its logo scales down as the page scrolls, using transform: scaleY instead of animating height. Use it on landing pages and blog headers that need to stay pinned without eating the whole screen.

64→48px로고만그림자 추가
.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 background drifts slower than the foreground content as the page scrolls, and the gap between the two speeds is what reads as depth. Use it for hero backgrounds and the breaks between sections.

0.5배0.3배두 겹
.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

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. No JavaScript reads the value; the CSS counter does. Use it for stats sections, anywhere a static number would look inert.

1.2s천 단위 콤마% 붙이기
@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

A vertical line draws itself as you scroll the page, and each dot lights up once the line has passed it. Use it for company history, step-by-step guides, and roadmaps, where progress itself is the point.

2px가로형점 없이
.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

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.

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.

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

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 CSS scroll effects live in the CSS category, and what this site actually is gets explained on the about page.

FAQ

Does this need any JavaScript at all?

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.