Skip to main content

Command Palette

Search for a command to run...

Mind Blowing Cut Page Animation in React

Updated
8 min read
Mind Blowing Cut Page Animation in React

We’re going to fake a page getting cut into vertical strips, then we’ll yank those strips up and down like blinds, revealing a brand-new scene underneath (in our case: a simple light-blue background).

https://codesandbox.io/embed/tfv76t?view=preview

The Concept: Paper Over a Picture

Imagine a white sheet of paper with text on it, placed over a colorful picture.

You take a blade and make vertical cuts through the paper.

Then you lift alternating strips — some up, some down — revealing the picture underneath.

That’s exactly what this animation does:

  1. Cutting phase: vertical lines animate in, simulating “cuts”

  2. Reveal phase: the “cut” strips slide away (up/down alternating)

  3. Scene phase: the new scene is fully visible

The Wrap That Makes It Possible

This line is the whole magic trick:

<AnimationOverlay isActive={isAnimationActive}>
  {content}
</AnimationOverlay>

What it means

  • AnimationOverlay is a wrapper component.

  • It receives your whole page (content) as children.

  • It decides how to render that page depending on the animation phase:

    • render normally (idle)

    • render the page + cutting lines (cutting)

    • render multiple clipped “slices” of the page (revealing)

    • render only the new scene (scene)

It’s basically a director standing behind the camera shouting:

“Okay, show the page… NOW cut it… NOW rip it apart… NOW show the scene.”

AnimationOverlay: The Orchestrator

1) idle

Nothing fancy.
Just render children like normal.

if (!isActive || phase === "idle") return <>{children}</>;

2) cutting

We render:

  • the page (children)

  • plus AnimatedLines in “cutting mode”

{children}
<AnimatedLines phase="cutting" onComplete={handleCuttingComplete} />

AnimatedLines draws vertical lines that slide into place, like blades.

When the last line finishes its animation, it calls onComplete, and the overlay moves to…

3) revealing

We render:

  • AnimatedLines in “revealing mode”

  • and pass the same children again

<AnimatedLines phase="revealing" onComplete={handleRevealComplete}>
  {children}
</AnimatedLines>

This is the big trick: in reveal mode, AnimatedLines doesn’t draw lines —
it creates multiple vertical slices of your page and animates them away.

Then we move to…

4) scene

We stop rendering the page at all.
Only the background “scene” remains.

In your demo it’s just light-blue, full screen

AnimatedLines: Two Personalities in One Component

AnimatedLines has two completely different behaviors depending on phase.

A) Cutting phase: vertical blades

We generate equal X positions across the screen:

const generateEqualCutPositions = (count, edgePaddingPct = 6) => ...

This creates a list like:

  • 6%

  • 14%

  • 22%

  • ...

  • 94%

So the cuts are evenly spaced.

Then we render each cut as a <div class="cut-line"> positioned by left: ${xPos}%.

We also alternate the direction:

const fromTop = i % 2 === 0;

So one line comes from the top, the next from the bottom, etc.

It looks more “alive” than all lines falling from the same direction.

To know when we’re done, we count animations ending:

const handleCutEnd = () => setCutsComplete((prev) => prev + 1);

When cutsComplete matches number of lines, we call onComplete() and advance to reveal.

B) Reveal phase: slicing the page into strips

This is the sneaky part.

We take all cut positions and build “sections”:

const createSections = (cutPositions) => {
  const all = [0, ...cutPositions, 100];
  return all.slice(0, -1).map((left, i) => ({
    left,
    width: all[i + 1] - left,
  }));
};

So if your cuts are at 10%, 30%, 50%...
Then sections become:

  • 0% → 10%

  • 10% → 30%

  • 30% → 50%

  • etc…

Each section renders the entire page again… but only shows its own slice.

That’s how we “cut” the page without actually cutting it.

The Clip-Path Trick

Each section uses:

clipPath: `inset(0 ${right}% 0 ${left}%)`

Why clip-path?

Because we want each slice to show only a vertical segment of the page.

We could try:

  • cropping with overflow + nested wrappers

  • manually splitting layout

  • rendering separate content

…but clip-path is clean: it masks the element visually while keeping layout intact.

Why inset() specifically?

clip-path: inset(top right bottom left) is perfect for “rectangular slicing”.

We want:

  • top = 0

  • bottom = 0

  • left = start of slice

  • right = everything beyond slice

So each slice becomes:

  • “show only this vertical window”

  • “hide the rest”

And because we define it in percentages, it scales nicely with viewport size.

This is why your reveal phase is possible without rewriting the page layout.

The “Goes Up / Goes Down” Alternation

For each slice:

const goesUp = i % 2 === 0;
"--direction": goesUp ? "-1" : "1"

Then CSS does:

transform: translateY(calc(var(--direction) * 100vh));

So slices alternate:

  • slice 0 → up

  • slice 1 → down

  • slice 2 → up

  • slice 3 → down

That alternating motion is what sells the illusion that the page was “cut”.

If they all went the same direction, it would feel like a normal page slide transition.

styles.css: Performance Corner (The Good Stuff)

will-change: transform;

You’ll see this on .cut-line and .section-reveal.

What it tells the browser:

“Hey, I’m about to animate transform. Please prepare for that.”

Browsers can then:

  • promote the element to its own compositor layer

  • reduce paint work during animation

  • avoid stutter when the animation starts

It’s basically pre-warming the engine.

Important caveat: don’t sprinkle will-change everywhere.
It consumes memory if abused.

Here it’s used on a few animated elements, so it’s a good fit.

Why we animate with transform: translateY(...)

We deliberately avoid animating layout properties like top, left, height.

Layout properties cause:

  • layout recalculation

  • paint

  • potential jank

Transforms are compositor-friendly:

  • they usually run on the GPU

  • no layout thrashing

  • smoother on weaker devices

That’s why our keyframes are:

@keyframes section-slide {
  0% { transform: translateY(0); }
  100% { transform: translateY(calc(var(--direction) * 100vh)); }
}

We’re pushing the slice off-screen by exactly one viewport height.

The section-slide animation

animation: section-slide var(--reveal-duration, 3000ms)
  cubic-bezier(0.16, 1, 0.3, 1) forwards;

Breakdown:

  • section-slide
    the keyframes that move the slice

  • var(--reveal-duration, 3000ms)
    uses a CSS variable if provided, otherwise defaults to 3000ms

  • cubic-bezier(0.16, 1, 0.3, 1) this easing is “fast start, slow settle” it makes the animation feel physical, not linear / robotic (it’s like: rip it hard, then let it drift)

  • forwards
    keeps the final transform applied otherwise, the strips would snap back when the animation ends
    (and that would destroy the illusion instantly)

A Small Line With Big Impact: contain: strict

At some point in the animation code you’ll see something like this:

<div style={{ contain: "strict" }}>
  {/* animated content */}
</div>

This single CSS property can dramatically reduce the amount of work the browser has to do during complex animations.

What contain Actually Means

contain is a CSS performance hint. It tells the browser:

“Everything inside this element is self-contained.
Nothing in here affects the outside world.”

In other words:

  • layout changes inside won’t affect layout outside

  • paint changes inside won’t require repainting ancestors

  • style recalculations are isolated

When you write:

contain: strict;

You are enabling all containment types at once:

contain: layout paint style size;

This is the strongest possible form of containment.

Why This Matters for Animations

During the reveal phase, we do something fairly aggressive:

  • render multiple copies of the entire page

  • clip each copy into vertical slices

  • animate every slice simultaneously

  • move them off-screen with transforms

Without containment, the browser has to consider the possibility that:

  • moving one slice might affect layout elsewhere

  • painting one slice might invalidate large areas

  • style changes might cascade upward

Even if it doesn’t actually happen, the browser still has to check.

That checking is where performance dies.

contain: strict tells the browser:

“Stop checking.
This subtree is sealed.”


What the Browser Can Skip

With contain: strict, the browser is free to:

  • Skip layout propagation
    Moving slices won’t trigger reflows outside the container

  • Limit paint invalidation
    Only the contained area needs repainting

  • Isolate style recalculation
    CSS changes don’t bubble up the DOM tree

This is especially important when:

  • you animate many elements at once

  • those elements are visually large

  • you duplicate content (like we do for slices)

Why This Pairs Perfectly With transform

Our animation already follows best practices:

  • only animates transform and opacity

  • avoids layout-affecting properties

  • uses will-change: transform

contain: strict completes the picture.

Think of it like this:

  • transform keeps animations on the compositor

  • will-change preps the GPU

  • contain limits the blast radius

Together, they prevent:

  • layout thrashing

  • unnecessary repaints

  • accidental main-thread work


When Not to Use contain: strict

This is not a free optimization.

You should not use contain: strict when:

  • the element’s size depends on its children

  • children need to affect surrounding layout

  • you rely on percentage sizing relative to ancestors

  • positioned elements need to escape the container

In our case:

  • the animation container is full-screen

  • its size is fixed

  • its contents are visually isolated

So it’s a perfect fit.

Mental Model

A good way to think about contain: strict:

“This is a mini universe.
Physics inside don’t leak out.”

For animation-heavy UIs, that’s exactly what you want.

Why This Matters More Than You Think

Most animation jank doesn’t come from bad easing curves.

It comes from:

  • browsers doing extra work you didn’t ask for

  • layout and paint costs you didn’t realise you triggered

contain: strict is you telling the browser:

“Relax. I’ve got this under control.”

And when paired with transform-based animations, it’s one of the cleanest performance wins you can get in modern CSS — especially in animations that look way more expensive than they actually are.