# 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](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:

```javascript
<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.

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

#### 2) `cutting`

We render:

* the page (children)
    
* plus `AnimatedLines` in “cutting mode”
    

```javascript
{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*
    

```javascript
<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:

```javascript
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:

```javascript
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:

```javascript
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”:

```javascript
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:

```javascript
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:

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

Then CSS does:

```javascript
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:

```javascript
@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

```javascript
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:

```javascript
<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:

```javascript
contain: strict;
```

You are enabling **all containment types at once**:

```javascript
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.
