---
name: clip-path-page-transition
description: Use when you want "Creative, premium, dynamic" - Uses CSS clipping shapes to transition between pages.
---

# Clip-Path Page Transition

> **Category:** Loading / Transition  -  **Personality:** Creative, premium, dynamic
>
> **Best use:** Portfolios, agencies, campaigns
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 4/5 - Performance cost 3/5

## What it is

A full-cover overlay sweeps across the viewport as an animated `clip-path` shape (here an angled parallelogram, not a plain circle), the page content is swapped while the overlay fully covers the screen, then the overlay sweeps off to reveal the new "page." It turns an ordinary navigation into a deliberate, choreographed moment. Use it on portfolios, agency sites and campaign pages where the *transition itself* is part of the brand. The demo runs it without any real navigation: clicking the nav swaps mock views inside one self-contained stage.

## Dependencies / CDN

None - pure CSS `clip-path` transitions + vanilla JS. The demo only loads display/body fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;700&family=Space+Mono:wght@400;700&family=Unbounded:wght@600;700;800&display=swap" rel="stylesheet">
```

## HTML

The stage holds the swappable views plus a transition overlay of two stacked panes (an accent pane and an ink pane that also carries the destination label):

```html
<div class="stage t-index" data-cpw>
  <header class="bar">
    <span class="brand">MERIDIAN</span>
    <nav>
      <button class="link" data-view="index" aria-current="page">Index</button>
      <button class="link" data-view="work">Work</button>
      <button class="link" data-view="contact">Contact</button>
    </nav>
  </header>

  <div class="views">
    <section class="view" data-view-panel="index">…current page…</section>
    <section class="view" data-view-panel="work" hidden>…</section>
    <section class="view" data-view-panel="contact" hidden>…</section>
  </div>

  <!-- transition overlay: two staggered clip-path panes -->
  <div class="overlay" aria-hidden="true">
    <span class="pane pane--accent"></span>
    <span class="pane pane--ink"><span class="label"></span></span>
  </div>
</div>
```

## CSS

The three `clip-path` states are the whole effect; the stagger and label are polish.

```css
.overlay{position:absolute; inset:0; z-index:9; pointer-events:none}

.pane{
  position:absolute; inset:0; will-change:clip-path;
  /* PARKED off-screen left (a right-leaning parallelogram, zero visible area) */
  clip-path:polygon(-160% 0, 0% 0, -30% 100%, -190% 100%);
  transition:clip-path .54s cubic-bezier(.76,0,.24,1);
}
.pane--accent{background:#d6f24a}
.pane--ink{background:#0f0e0c; display:grid; place-items:center; transition-delay:.09s}

/* ENTER — sweep in to FULLY COVER the stage (accent leads, ink trails 90ms) */
.stage.is-cover  .pane{clip-path:polygon(0% 0, 160% 0, 130% 100%, -30% 100%)}

/* EXIT — sweep off to the right to reveal (ink leads out, accent trails) */
.stage.is-reveal .pane{clip-path:polygon(130% 0, 290% 0, 260% 100%, 100% 100%)}
.stage.is-reveal .pane--ink{transition-delay:0s}
.stage.is-reveal .pane--accent{transition-delay:.09s}

/* used to snap the panes back to PARKED without animating, ready to replay */
.stage.cpw-noanim .pane{transition:none}

/* the destination name shows on the ink pane and gets wiped in/out with it */
.label{font-family:'Unbounded',sans-serif; font-weight:800; color:#f1ece1;
  font-size:clamp(40px,9vw,94px); letter-spacing:-.04em; text-align:center}

/* per-page theme is just a set of CSS vars swapped on the stage at swap time */
.stage.t-work   {--bg:#0f0e0c; --fg:#f1ece1; --acc:#d6f24a}
.stage.t-contact{--bg:#d6f24a; --fg:#141008; --acc:#141008}

@media (prefers-reduced-motion: reduce){ .pane{transition:none} }
```

## JavaScript

Click a nav item → cover → swap the view at full cover → reveal → reset the panes off-screen for the next run. Timings mirror the CSS (`.54s` + `.09s` stagger).

```js
(function(){
  var stage = document.querySelector('[data-cpw]');
  var links = [].slice.call(stage.querySelectorAll('.link'));
  var views = [].slice.call(stage.querySelectorAll('.view'));
  var label = stage.querySelector('.label');
  var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  var current = 'index', busy = false;
  var DUR = 540, STAGGER = 90, HOLD = 70;          // keep in sync with the CSS

  function apply(view){                             // swap content + nav + theme
    views.forEach(function(v){ v.toggleAttribute('hidden', v.dataset.viewPanel !== view); });
    links.forEach(function(l){
      l.toggleAttribute('aria-current', l.dataset.view === view);   // (set/remove "page")
    });
    stage.classList.remove('t-index','t-work','t-contact');
    stage.classList.add('t-' + view);
    current = view;
  }

  function go(view){
    if(busy || view === current) return;
    if(reduce){ apply(view); return; }              // no wipe under reduced motion
    busy = true;
    label.textContent = view;                       // destination name on the ink pane

    stage.classList.add('is-cover');                                  // sweep in
    setTimeout(function(){ apply(view); }, DUR);                      // swap while covered
    setTimeout(function(){                                            // sweep away
      stage.classList.remove('is-cover'); stage.classList.add('is-reveal');
    }, DUR + STAGGER + HOLD);
    setTimeout(function(){                                            // park for replay
      stage.classList.add('cpw-noanim');
      stage.classList.remove('is-reveal');
      void stage.offsetWidth;                       // force reflow so reset doesn't animate
      stage.classList.remove('cpw-noanim');
      busy = false;
    }, DUR + STAGGER + HOLD + DUR + STAGGER + 40);
  }

  links.forEach(function(l){ l.addEventListener('click', function(){ go(l.dataset.view); }); });
})();
```

## How it works

- **One overlay, three states.** Each pane is a right-leaning parallelogram defined with `polygon()`. It lives **parked off-screen left** (zero visible area), animates to a **full-cover** state (the shape over-extends past 100% so every corner is covered), then to an **off-screen right** state to reveal. Interpolating the four points produces the diagonal sweep.
- **Swap under cover.** A `setTimeout` fires at the cover duration — by then the leading (accent) pane fully covers the stage, so swapping the visible view, the active nav and the theme is invisible to the user.
- **Two panes = depth.** The accent pane leads and the ink pane trails by 90ms (`transition-delay`); on exit the order flips. The thin offset between them reads as a premium two-tone wipe instead of a flat bar.
- **Replay reset.** After the reveal, `clip-path` is snapped back to the parked state with transitions disabled (`cpw-noanim` + a forced reflow), so the next click starts clean — no rewind animation.
- **Theme = CSS vars.** Each "page" is just a class (`t-work`, `t-contact`…) that swaps `--bg/--fg/--acc`; the persistent header recolors along with the content under the cover.

## Customization

- **Shape:** swap the three `polygon()` values for blinds (multiple thin panes), an angular chevron, or a steeper diagonal — keep "parked" and "off-right" at zero visible area and make "cover" over-extend past `100%`.
- **Direction:** mirror the X coordinates to wipe right-to-left; swap X/Y to wipe vertically.
- **Pace:** tune `DUR`/`STAGGER`/`HOLD` (and the matching CSS `transition` / `transition-delay`). Keep them in sync or the swap will peek.
- **Easing:** `cubic-bezier(.76,0,.24,1)` is a punchy in-out; soften to `(.4,0,.2,1)` for calmer brands.
- **Panes:** drop to one pane for a minimal wipe, or add a third for richer layering. The label is optional.

## Accessibility & performance

- `clip-path` animates on the compositor and is cheap; this stays smooth where blurred/`box-shadow` transitions would jank.
- Honour `prefers-reduced-motion`: the JS swaps content instantly (no wipe) and the CSS drops the pane transition.
- Keep nav items real `<button>`s so they are focusable and keyboard-operable; mark the active one with `aria-current="page"`.
- The overlay is `aria-hidden="true"` and `pointer-events:none`, so it never traps focus or blocks clicks. A `busy` flag ignores clicks mid-transition.
- In a *real* multi-page site, run the "cover" half, then navigate; play the "reveal" half on the next page's load (e.g. via the View Transitions API or a small inline script) so the wipe spans the page change.

## Gotchas

- **Cover must truly cover.** Because the shape is slanted, the cover polygon has to push past the edges (e.g. `160%`, `-30%`) or a triangular sliver of the old page leaks at a corner.
- **Resetting will animate** unless you disable the transition first. Snap to "parked" inside a `no-anim` class and force a reflow (`void el.offsetWidth`) before re-enabling — otherwise the bar visibly slides back across.
- **Swap timing is load-bearing.** Swap content while the *leading* pane fully covers (at `DUR`), not after the trailing pane finishes, or users glimpse the change.
- **Contain the overhang.** The panes extend past `100%`; put `overflow:hidden` on the stage (and `inset:0` panes) so the over-extended shape never spawns a horizontal scrollbar.
- **A trailing pane that matches the destination background** looks like nothing on exit (e.g. a lime pane revealing a lime page). That can be elegant, or swap the pane colour if you want the wipe to stay visible.
