Image Reveal with a View Timeline and clip-path

A clip-path reveal uncovers an image progressively from one edge as it enters the viewport, which reads differently from a fade: a fade says the picture arrived, a wipe says it is being drawn. Both are compositor-safe and neither repaints the image, so the choice is editorial rather than technical — but the wipe has two details that catch people out, in the inset direction and in the reduced-motion reset. This guide covers both. It sits under scroll-driven media and gallery effects.

When to use this approach

  • Editorial photography where the composition should be discovered. A landscape uncovered along its horizon, a portrait uncovered upward — the direction can follow the picture’s own geometry in a way a fade cannot.
  • Sections where several images arrive in sequence. A directional wipe gives each one a sense of order that simultaneous fades do not.
  • When you want a reveal that is unmistakably deliberate. A fade is easy to miss; a wipe is not, which is a reason to use it sparingly.

Prefer a plain opacity fade when the image is decorative, when the shape is irregular, or when the section already carries several other effects — the wipe is the more attention-seeking of the two, and attention is a budget.

Which inset edge to animate A clip-path inset takes four values in the order top, right, bottom, left. Animating the right value from one hundred percent to zero uncovers the image from the left. Animating the bottom value uncovers it upward. The value you animate is the edge the cover retreats to, not the edge it starts from. inset(T R B L) top right bottom left Reveal direction inset(0 100% 0 0) → inset(0 0 0 0) uncovers rightward inset(0 0 100% 0) → inset(0 0 0 0) uncovers downward inset(100% 0 0 0) → inset(0 0 0 0) uncovers upward Keep both endpoints the same shape function, or the interpolation degrades to a discrete swap.

Implementation

1. Reserve the box before anything else

.reveal img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
}

Without a reserved box the image has no dimensions until it loads, so the view timeline measuring it re-derives when it arrives — and everything below shifts. This step is not optional for any media effect.

2. Write the finished state as the baseline

.reveal img { clip-path: none; }

This is what an engine without timeline support renders, and what a reduced-motion reader sees. Getting it right first means the enhancement can only add.

3. Add the wipe behind a feature query

@supports (animation-timeline: view()) {
  .reveal img {
    clip-path: inset(0 100% 0 0);
    animation: wipe-right linear both;
    animation-timeline: view(block);
    animation-range: entry 15% entry 85%;
  }

  @keyframes wipe-right {
    to { clip-path: inset(0 0 0 0); }
  }
}

The animated value is the right inset going from full to zero, which means the covered region retreats rightward and the image is uncovered from the left.

4. Range it to finish on arrival

Ranging a reveal so it finishes in view A subject travelling through the scrollport. A reveal ranged across the whole cover span finishes as the element is already leaving. A reveal ranged from entry fifteen to entry eighty-five percent completes while the element is arriving, so the reader sees a finished image rather than one still uncovering. enters fully visible leaves cover 0% → 100% still uncovering as it leaves the screen entry 15% → 85% done on arrival the reader sees a finished image A reveal is an arrival effect. If it is still running when the reader starts looking at the picture, the range is too long — not the duration, which a scroll-driven animation does not have.

A reveal spread across the whole cover range is still uncovering while the reader is already looking at the picture. entry 15% to entry 85% finishes as the element completes its arrival, which is the behaviour a reveal is trying to express.

5. Reset both halves for reduced motion

@media (prefers-reduced-motion: reduce) {
  .reveal img {
    animation: none;
    clip-path: none;      /* without this the image stays hidden */
  }
}
Resetting a clip reveal correctly Two reduced-motion resets compared. Setting animation to none alone leaves the initial clip-path applied, so the image stays permanently hidden. Resetting both the animation and the clip-path restores the complete image, which is the state the effect was working toward. animation: none only still clipped — 92% hidden image effectively removed animation + clip-path reset fully visible the intended end state Any reveal whose initial state hides content needs both halves of the reset. This applies equally to opacity: 0, scale: 0 and translate-based reveals — the animation is not what is hiding the element.

Verification

Confirm the image is complete with the enhancement disabled. Comment out the @supports block, reload, and check that the figure renders normally — if it does not, the baseline is wrong and every unsupported engine gets a broken page.

Then check the reveal finishes in view. Scroll slowly and note where the wipe completes relative to the element’s position; if it is still running once the image is centred, tighten the range rather than reaching for easing.

Finally, emulate reduced motion and confirm the image is fully visible rather than clipped. This is the failure that ships most often, because the effect looks correct in every other state.

Edge cases and gotchas

Mismatched shape functions. clip-path interpolates only between shapes of the same type with the same argument count. An inset() to a polygon() swaps discretely at the halfway point, which looks like a bug and is specified behaviour.

Rounded corners on the clip. inset(0 100% 0 0 round 12px) is valid and interpolates, but the radius is applied to the clipped rectangle, so a partly-revealed image shows a rounded edge travelling across it. That is occasionally the intent and more often a surprise.

Clipping a container rather than the image. Applying the clip to the <figure> clips the caption too. Apply it to the image, or accept that the caption participates.

Decode landing inside the reveal. A large image that begins decoding as the reveal starts drops the opening frames. Reserve the box, keep the served size close to the displayed size, and give lazy loading a generous threshold so the decode finishes first.

Overflow on an ancestor. clip-path clips the element’s own painting; it does not create a scroll container or interact with overflow. An ancestor with overflow: hidden still clips independently, which can produce a doubly-clipped result during the animation.

Browser-specific notes

clip-path with inset() is supported in every engine relevant to scroll-driven work, and all three interpolate same-shape insets smoothly on the compositor.

Chromium composites animated clip-path without repainting the element, which is what makes this pattern cheap. Very large images still cost texture memory as usual, but the clip itself is free per frame.

Safari also composites the clip, and its behaviour with round radii matches Chromium. Its earlier view() builds had inset-argument parsing bugs in the timeline function, so expressing the timeline inset via the view-timeline-inset property rather than inside view() is the more portable form.

Firefox composites animated clip-paths as well, and shipped the timeline functions most recently of the three. Nothing in this pattern needs an engine-specific branch — a single @supports guard covers it.

Frequently Asked Questions

Should the wipe direction match the reading direction?

Usually, and it is worth being deliberate about it. A wipe that runs against the reading direction reads as slightly uncomfortable in the same way that text sliding in from the wrong side does. In a right-to-left interface that means mirroring the direction, which the logical inset order does not do for you — inset() values are physical, so a direction-aware custom property is the simplest fix.

Can the reveal run in both directions as the reader scrolls back?

It does automatically. A scroll-driven animation is scrubbed rather than played, so scrolling upward re-covers the image. Whether that is desirable depends on the effect: a wipe that re-covers reads as reversible and honest, while for a reveal meant to be a one-time arrival it can feel like the content is being taken away. There is no fill mode that makes a scroll-driven animation one-shot; if you need that, an IntersectionObserver toggling a class is the right tool.

Does a clip-path reveal affect the image’s accessibility?

No. The image stays in the accessibility tree with its alt text intact, and clipping affects painting only. The one accessibility consideration is the reduced-motion reset, because a reset that leaves the initial clip in place hides the image from everyone who set the preference — which is a genuine content loss rather than a cosmetic one.

Is this cheaper than animating opacity?

They are comparable — both are compositor-only and neither repaints. Opacity is marginally simpler for the compositor, but the difference is not measurable at the scale of a page. Choose between them on how the effect reads, not on cost.

Can several figures on a page share one keyframe set?

Yes, and they should. The wipe direction is the only thing that usually varies between figures, and expressing it as a custom property inside the keyframes lets one rule cover the whole page while each figure sets its own direction. Duplicating the keyframes per direction is the version that drifts out of sync when the range is later adjusted.

The same applies to the range. A single animation-range on the shared rule keeps every reveal on the page timed alike, which is what makes a set of them read as one design decision rather than as several.

Does the reveal delay the largest contentful paint?

It can, if the clipped image is the largest element on the page and the clip hides it at load. The metric records when the largest element is painted in its final form, and a fully-covered image has not been. Restricting reveals to images below the fold — which is where they belong anyway, since an above-the-fold reveal has no scroll to drive it — avoids the interaction entirely.


Up: Scroll-Driven Media & Gallery Effects