Skip to content
CHICKENPIE.Start a project →
Making things

Make Anything Scroll Into View with Pure CSS (No JavaScript)

Four lines of CSS. No JavaScript, no npm, no IntersectionObserver. Here's how to build scroll-reveal animations that run on the compositor thread and work in every major browser.

Read the paper ↓
Mr. Chicken sitting at a home office desk in a zip-up hoodie, grinning at a monitor showing colorful cards scrolling upward into view — Chickenpie flat-vector illustration
A PAPER FROM THE DESK
THE DETAILS
At a glance
CategoryMaking things
PublishedJul 27, 2026
Read time5 min
Covered here
  • Design
  • Productivity
Client
Chickenpie Tutorial
Year
2026
Services
Web Design · CSS Animation · Front-End Development

The challenge

Aspiring creatives want scroll-reveal animations on their portfolios but hit a wall every time they try — the JavaScript approach (IntersectionObserver, animation libraries, npm packages) is overkill for a simple portfolio page and breaks in unpredictable ways on mobile.

The approach

CSS's native scroll-driven animations API (animation-timeline: view()) replaces all of that with four lines of code that run on the compositor thread, require no dependencies, and degrade gracefully on the 16% of browsers that haven't shipped full support yet.

The result

  • Build a working scroll-reveal effect using only CSS — no JavaScript required
  • Understand animation-range phases (entry, cover, exit) so you can tune the trigger point
  • Add a stagger effect with CSS custom properties
  • Write a proper @supports fallback that keeps the page usable on all browsers
  • Know which CSS properties are compositor-safe and which will tank your scroll performance

Your portfolio card slides up and fades in as the reader scrolls past it. Three months ago that meant installing a JavaScript library, setting up an IntersectionObserver, and hoping nothing broke on mobile. Now it's four lines of CSS.

By the end of this tutorial, you'll have a working scroll-reveal animation — elements that fade and translate up as they enter the viewport — using CSS's native animation-timeline: view(). No npm. No script tags. No IntersectionObserver. Chrome 115+, Edge 115+, and Safari 18+ all support this natively, covering about 84% of global browsers right now. It won't be perfect on that 16% yet. We'll cover the fallback.

Prerequisites

Comfortable with HTML and CSS — if you can style a card, you're ready. A text editor and Chrome or Edge open next to it. About 30 minutes.

Diagram showing a browser viewport frame with three card shapes rising upward from the bottom edge, with motion lines and labels — Mr. Chicken pointing at the cards

Step 1: Build the base HTML

Start with a simple page — a stack of cards with plenty of content to scroll through. Add enough cards that the page actually scrolls — five or six is good. Set min-height: 100vh on the body and space out your cards with margin-bottom: 20vh. You need room to scroll for the effect to be visible.

Step 2: Write the @keyframes animation

Define a standard animation — the kind you'd use for any fade-in. Use the translate property (not transform: translateY()). The newer shorthand is cleaner and plays nicer with compositor-thread rendering. Common mistake: animating margin-top or height instead of translate. Those trigger layout recalculation on every frame. translate and opacity are the only two properties guaranteed to run off the main thread.

Step 3: Bind it to the viewport with animation-timeline: view()

This is the part that used to require JavaScript. Set animation: fade-up linear both on your .card element, then add animation-timeline: view() and animation-range: entry 0% entry 30%. Three things are happening: the animation runs without a duration (scroll position replaces time), the browser drives it with the element's viewport position instead of a clock, and the animation plays as the element enters the viewport from 0% to 30% visible. Save and refresh. Scroll down. The cards fade up as they enter the frame.

Diagram showing a vertical scroll progress track with 0%, 30%, and 100% markers, and a card element at three opacity stages — transparent, half-visible, and solid

Step 4: Tune the animation-range

The entry 0% entry 30% range is your main dial. entry 0% is the moment the element's bottom edge crosses the viewport bottom. entry 30% is when 30% of the element is visible. If 30% feels too fast, push it to entry 50%. For a more cinematic feel, try entry 10% cover 30% — it delays the start slightly and finishes when the element covers 30% of the viewport. The entry phase isn't the only option: there's also cover, contain, exit, and exit-crossing. For most scroll-reveals, entry is all you need.

Step 5: Add a stagger

Right now all cards animate simultaneously, which flattens the effect when multiple cards are on screen. The clean approach: set animation-delay: calc(var(--i, 0) * 100ms) on .card, then put --i:0, --i:1, --i:2 as inline style attributes on each card. Now you have a waterfall without repeating the selector for every nth-child.

Step 6: Lock the state with animation-fill-mode: both

If you scroll fast, an element can enter the viewport mid-animation and jump. The both keyword in your animation shorthand handles this — it applies the from state before the animation starts (elements are invisible before they enter) and holds the to state after it ends. You already added it in Step 3. Check that it's there: animation: fade-up linear both.

Step 7: Add the fallback for the 16%

Firefox stable (as of mid-2026) is still rolling out full support. Wrap your scroll animation in @supports (animation-timeline: view()) { ... }. Without this, browsers that don't understand animation-timeline will still play the fade-up keyframe — but they'll play it immediately on load, not on scroll. The @supports block prevents that. Older browsers see your cards fully visible and static. That's a fine fallback.

Step 8: Compositor-safe properties only

Your animation runs smooth. Keep it that way. Only animate transform properties (translate, rotate, scale) and opacity. Avoid width, height, padding, margin — they trigger layout. Avoid background-color and border-color — they trigger paint. box-shadow triggers both. If your design calls for a color change on reveal, fake it: animate opacity on a pseudo-element with the target color overlaid. Looks the same, costs nothing.

If you get stuck

Cards not animating at all: check that you're in Chrome 115+ or Edge 115+. Firefox stable still needs a flag (layout.css.scroll-driven-animations.enabled in about:config). Cards animating but stuck halfway: you likely have overflow: hidden on a parent. view() tracks position relative to the nearest scroll ancestor — remove the overflow or use view(block) to scope it to the root scroller. Cards not visible on mobile Safari before scrolling: this is an animation-fill-mode issue combined with iOS's momentum scroll. Fix it by setting .card opacity: 1 by default and moving the opacity: 0 reset inside the @supports block — the hidden state is opt-in, visible is the fallback. Animation plays twice: you have two scroll containers. Explicitly set animation-timeline: view(block) to bind to the root scroller only.

Next tutorial: We'll use the same technique to build a reading-progress bar — the kind that fills across the top of the page as you scroll through an article. One element. Eight lines of CSS.

Share
FROM IDEA TO SOMETHING REAL

What are you making?

Graphics, websites, and curious experiments. Let’s talk about yours.

Talk to the studio ↗
/feed/css-scroll-reveal-pure-css

Have a project in mind?

Tell us what you are making and what you need. An unfinished idea is welcome too.

Start a project →

Another paper for you.

Mr. Chicken at a desk pointing at a laptop screen showing a colorful AI-generated image, with reference photos stacked nearby

6 AI Art Tool Tips That Stop Your Marketing Images From Screaming "AI"

6 quick tips for using AI art tools like Nano Banana and Gemini 3 to make marketing visuals that don't scream "AI" — prompting, texture, consistency, and a pre-publish check.

Open paper
Chickenpie mascot Mr. Chicken pointing at a glowing screen showing chaotic color swatches organizing into labeled design system tokens

Stop Hardcoding Colors: Build a Real Design System with Figma Variables

A hands-on 9-step tutorial: wire a Primitives, Semantic, and Brand variable system in Figma so your whole file updates from one place, light mode and dark mode included.

Open paper
Mr. Chicken at a cluttered home studio desk, working on an AI image generation interface with an uncanny AI-slop draft on one side and a clean composited image on the other

How to Art-Direct AI Images So They Stop Looking Like AI

Nine steps to art-direct AI image generation like a photographer, not a wishlist — light, texture, junk tokens, and the two spots (hands, signage) that always give it away.

Open paper
Mr. Chicken at a nighttime desk poking a laptop screen showing a bouncy Lottie squeeze-toy animation

I Tried to Build a Clickable Lottie Squeeze Toy. It Took Three Tries to Feel Right.

Watch three failed attempts to build an interactive Lottie squeeze toy — from a dead mousemove hack to a broken freeze-frame click, to the segment-based fix that finally feels tactile.

Open paper
Mr. Chicken at a cluttered design studio desk working on a product composite in Photoshop

How to Composite a Product into Any Background Using Photoshop Generative Fill

Drop a product into any background in 10 steps using Photoshop Generative Fill — with matching light, shadow, and zero obvious AI tells.

Open paper
Mr. Chicken at a studio desk with two monitors — Figma design frames on the left, live Framer website on the right with a glowing Publish button

Your Figma File Is Already a Website. Here's How to Publish It in 8 Steps.

Stop waiting for a developer. Here's an 8-step walkthrough for taking your Figma design into Framer and publishing a live website today — no code required.

Open paper