---
name: clip-path-reveal
description: Use when you want "Creative, modern, polished" - Uses CSS clip-path to reveal content in geometric or organic shapes.
---

# Clip-Path Reveal

> **Category:** Image / Media  -  **Personality:** Creative, modern, polished
>
> **Best use:** Hero sections, cards, galleries
>
> **Ratings:** Professional 4/5 - Casual 3/5 - Exotic 4/5 - Visual impact 4/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

Clip-Path Reveal uncovers an image (or any element) by **animating its `clip-path`** from a small/closed shape to a larger one. Because `clip-path` is interpolatable, a single transition can iris a `circle()` open, sweep a `polygon()` out to the full frame, or morph an `inset()` lozenge into a square — all on the GPU with no extra DOM. It reads as deliberate and editorial, which makes it ideal for hero entrances (fired on scroll) and for gallery tiles that morph their shape on hover. In the demo, an aperture `circle()` opens the hero on scroll / Replay while three tiles morph `polygon()`, `circle()` and `inset()` on hover.

## Dependencies / CDN

**None - pure CSS** for the effect itself (`clip-path` + `transition`). A tiny bit of vanilla JS arms the start state and fires the reveal on scroll / on a Replay click — no library. The demo also loads display 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=Bodoni+Moda:ital,wght@0,500;0,600;0,700;1,500&family=Hanken+Grotesk:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
```

## HTML

A scroll/Replay-revealed feature image, plus hover-morph tiles. The `data-obs` hook and the `obs-feature__img` class are what the CSS/JS target.

```html
<div class="obs-stage" data-obs>
  <!-- HERO: clip-path iris, revealed on scroll / Replay -->
  <figure class="obs-feature">
    <img class="obs-feature__img" src="/img/landscape-mountain.jpg" alt="Snow-lit ridge">
  </figure>
  <button class="obs-replay" type="button" data-replay>Replay reveal</button>

  <!-- TILES: each rests in a shape, morphs to full on hover -->
  <div class="obs-grid">
    <figure class="obs-tile obs-tile--hex"><img src="/img/architecture-01.jpg" alt="Facade"></figure>
    <figure class="obs-tile obs-tile--iris"><img src="/img/landscape-coast.jpg" alt="Coast"></figure>
    <figure class="obs-tile obs-tile--inset"><img src="/img/street-01.jpg" alt="Harbour"></figure>
  </div>
</div>
```

## CSS

```css
/* ---- HERO: aperture iris, opened by toggling a class ---- */
.obs-feature__img{
  width:100%; height:100%; object-fit:cover; border-radius:16px;
  clip-path:circle(150% at 50% 48%);                 /* DEFAULT = fully open (safe, no-JS) */
  transition:clip-path 1.05s cubic-bezier(.16,.84,.32,1);
  will-change:clip-path;
}
.obs-armed   .obs-feature__img{ clip-path:circle(0%   at 50% 48%); }  /* closed start state */
.obs-revealed .obs-feature__img{ clip-path:circle(150% at 50% 48%); } /* animate open */

/* ---- TILES: rest shape  ->  hover = full frame (same function, matching params) ---- */
.obs-tile img{ width:100%; height:100%; object-fit:cover;
  transition:clip-path .7s cubic-bezier(.16,.84,.32,1), transform .7s ease; }

.obs-tile--hex   img{ clip-path:polygon(25% 0%,75% 0%,100% 50%,75% 100%,25% 100%,0% 50%); }
.obs-tile--iris  img{ clip-path:circle(34% at 50% 50%); }
.obs-tile--inset img{ clip-path:inset(13% 13% 13% 13% round 40px); }

.obs-tile--hex:hover   img{ clip-path:polygon(0% 0%,100% 0%,100% 50%,100% 100%,0% 100%,0% 50%); }
.obs-tile--iris:hover  img{ clip-path:circle(74% at 50% 50%); }
.obs-tile--inset:hover img{ clip-path:inset(0% 0% 0% 0% round 14px); }

/* snap the armed state into place without animating the collapse */
.obs-noanim *{ transition:none !important; }

@media (prefers-reduced-motion: reduce){ .obs-feature__img, .obs-tile img{ transition:none; } }
```

## JavaScript

Arms the closed start state, then plays the reveal when the stage scrolls into view. The Replay button re-runs it. Hover morphs are pure CSS — no JS.

```js
(function(){
  var stage = document.querySelector('[data-obs]');
  if(!stage) return;
  // Reduced motion: leave everything at its fully-open default — no animation.
  if(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;

  function fire(){
    stage.classList.add('obs-noanim');
    stage.classList.remove('obs-revealed');  // snap back to the closed (armed) state
    void stage.offsetWidth;                   // reflow so the reset doesn't animate
    stage.classList.remove('obs-noanim');
    stage.classList.add('obs-revealed');      // play the reveal
  }

  // Arm the closed start state without animating the collapse.
  stage.classList.add('obs-noanim','obs-armed');
  void stage.offsetWidth;
  stage.classList.remove('obs-noanim');

  if('IntersectionObserver' in window){
    var io = new IntersectionObserver(function(es){
      es.forEach(function(e){ if(e.isIntersecting){ fire(); io.disconnect(); } });
    }, {threshold:.28});
    io.observe(stage);
  } else { fire(); }

  var replay = stage.querySelector('[data-replay]');
  if(replay) replay.addEventListener('click', fire);
})();
```

## How it works

- **`clip-path` clips the paint box to a shape.** Whatever falls outside the shape is hidden; the element keeps its layout box, so nothing reflows while it animates.
- **Two states, one transition.** The image's *default* `clip-path` is the fully-open shape (so it's visible with no JS). JS adds `.obs-armed` to swap in the closed shape, then `.obs-revealed` to transition back open. The `.obs-noanim` reflow trick lets you jump to the closed state instantly so the open animation always starts clean (and Replay re-runs cleanly).
- **Smooth morphs need matching shape grammar.** The browser only interpolates between the *same* clip function with the *same* number of points/parameters. The hexagon (6 points) morphs to a rectangle expressed as a 6-point polygon; `circle()`→`circle()` animates its radius; `inset()`→`inset()` animates its offsets and `round` radius. You cannot tween `polygon()`→`circle()`.
- **Hover vs. scroll are independent.** Tile hover morphs are plain `:hover` CSS; the hero entrance is the class-driven state machine. They never fight because they target different elements/triggers.

## Customization

- **Shape vocabulary:** swap the geometry — `circle()` (iris/aperture), `ellipse()`, `inset(... round)` (rounded card), or `polygon()` for diagonals, chevrons, blobs and organic outlines. Tools like Clippy help author polygons.
- **Direction & origin:** move a circle's `at x% y%` to iris from a corner; change `inset()` sides to wipe from one edge; reorder polygon points to sweep diagonally.
- **Feel:** the easing carries the personality — `cubic-bezier(.16,.84,.32,1)` is a soft "settle". Pair the clip with a small `transform:scale()` for a subtle push, and stagger multiple elements with `transition-delay`.
- **Trigger:** fire on `IntersectionObserver` (scroll), on hover, on click, or on load.

## Accessibility & performance

- `clip-path` animates on the compositor and is cheap — far lighter than animating `width`/`height` or a WebGL mask. Keep `will-change:clip-path` only on the few elements actively animating.
- **Reduced motion:** the script returns early under `prefers-reduced-motion: reduce`, so the default (fully-open) state shows immediately with no animation.
- **No-JS / progressive enhancement:** because the *default* CSS shows the open shape, the content is fully visible even if the script never runs — JS only adds the reveal.
- Don't hide meaningful content behind a clip that may never open; the closed state should be decorative, not the only path to information.

## Gotchas

- **A point gets stranded if vertex counts differ.** To morph a polygon to a full rectangle, write the rectangle with the *same* point count and order as the start shape, or the transition snaps instead of tweening.
- **Percentages resolve against the element's box**, not the image's intrinsic size — `circle(74%)` covers a square but may clip the corners of a tall rectangle; size the radius (or use `closest-side`/`farthest-corner`) to the box you actually have. Keyword radii don't interpolate with `%`, so use explicit values on both ends.
- **`overflow:hidden` is not `clip-path`.** Rounded corners via `border-radius` still apply *with* the clip; if you over-open a `circle()` past the box, the visible result is just the rounded rectangle — handy for ending an iris on a clean frame.
- **Replaying a transition needs a reflow.** Toggling the class off and back on in the same frame does nothing without a forced reflow (`void el.offsetWidth`) between the two writes.
- Old Safari needs the **`-webkit-clip-path`** prefix for some shapes; add it if you must support legacy WebKit.
