1 Pure CSS6 with JSPublished 2026-10-05Free · no sign-up
Seven ways to bring an image on screen: a wipe as it scrolls into view, a coloured block that sweeps across, an iris that opens from the centre, slats that open like blinds, a photo that sharpens into focus, a diagonal edge and an image followed by its caption.
The image is uncovered from left to right by animating clip-path: inset() when it scrolls into the panel. An IntersectionObserver adds the class once; until then the image is clipped only if the script has run, so it is never stuck hidden.
View code
<div class="ir1" tabindex="0" role="region" aria-label="Scroll this panel"><p>Scroll ↓</p><figure><div class="im" role="img" aria-label="Terracotta rooftops at sunset"></div><figcaption>Rooftops, Seville</figcaption></figure></div>
var box = document.querySelector('.ir1'), fig = box.querySelector('figure'); box.classList.add('js');
var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { fig.classList.add('in'); io.disconnect(); } }, { root: box, threshold: .4 });
io.observe(fig);
02. Colour Block Curtain Reveal
RevealTwo-step
A two-step reveal used on portfolio sites: a coloured block sweeps in to cover the frame, then sweeps out the other side, leaving the image behind it. The image itself fades in at the midpoint, timed with an animation delay.
View code
<figure class="ir2"><div class="im" role="img" aria-label="Green hills under clouds"></div><span class="blk" aria-hidden="true"></span><button>Replay</button></figure>
var f = document.querySelector('.ir2');
f.querySelector('button').onclick = function () { f.querySelectorAll('.im,.blk').forEach(function (e) { e.style.animation = 'none'; void e.offsetWidth; e.style.animation = ''; }); };
03. Circle Reveal from the Centre
RevealClip-path
The picture opens as a growing circle from its centre, like a camera iris, with clip-path: circle() animating from 0 to 75 percent. The radius needs to reach the corners, which on a 4:3 frame is about 63 percent, so 75 leaves a margin.
View code
<div class="ir3"><div class="im" role="img" aria-label="Turquoise bay seen from above"></div><button>Replay</button></div>
var im = document.querySelector('.ir3 .im');
document.querySelector('.ir3 button').onclick = function () { im.style.animation = 'none'; void im.offsetWidth; im.style.animation = ''; };
04. Venetian Blinds Reveal
RevealStagger
Five horizontal slats cover the image and shrink away one after another like blinds opening. Each slat is a list-free span with an index for its delay, all sharing one keyframe that scales them to zero height.
The image arrives blurred and slightly zoomed and sharpens into focus as it enters the scroll panel, like a photo developing. It uses a view() timeline where supported, so the effect follows the scroll exactly, and shows the sharp image elsewhere.
View code
<div class="ir5" tabindex="0" role="region" aria-label="Scroll this panel"><p>Scroll ↓</p><div class="im" role="img" aria-label="Field of lavender"></div><p class="end">End of panel.</p></div>
A slanted edge sweeps across the frame, uncovering the image along a diagonal. clip-path: polygon() animates between two four-point shapes, which is allowed because both polygons have the same number of points.
var im = document.querySelector('.ir6 .im');
document.querySelector('.ir6 button').onclick = function () { im.style.animation = 'none'; void im.offsetWidth; im.style.animation = ''; };
07. Image and Caption in Sequence
RevealStagger
The image slides up first and its caption follows a moment later, a small choreography that makes a gallery feel considered. Both use the same keyframe with different delays, and the IntersectionObserver triggers the whole figure at once.
View code
<div class="ir7" tabindex="0" role="region" aria-label="Scroll this panel"><p>Scroll ↓</p><figure><div class="im" role="img" aria-label="Hand-thrown blue bowl"></div><figcaption><b>Cobalt bowl</b> · Stoneware, 2026</figcaption></figure></div>
var box = document.querySelector('.ir7'), fig = box.querySelector('figure'); box.classList.add('js');
var io = new IntersectionObserver(function (es) { if (es[0].isIntersecting) { fig.classList.add('in'); io.disconnect(); } }, { root: box, threshold: .3 });
io.observe(fig);
About these CSS Image Reveal Animations
clip-path does the heavy lifting
Most reveals animate clip-path: inset() for wipes, circle() for irises and polygon() for angled edges. Polygons can animate as long as the start and end shapes have the same number of points.
Hide only when the script is ready
If a reveal starts from a clipped or transparent image, add that starting state with a class that the script sets, or inside @supports for CSS timelines. A failed script must never leave an image hidden.
Images still need alt text
These demos use gradients with role="img" and an aria-label as stand-ins. With real images use <img alt>; the reveal animation does not change what the image needs to say.
Once is enough
Reveal an image the first time it enters view and then leave it alone. Disconnect the IntersectionObserver after it fires, and skip the effect entirely for reduced motion.
Frequently asked questions
Do reveal animations hurt Largest Contentful Paint?
They can if applied to the hero image. Use them below the fold, where the image is not the LCP element.
Can I reveal on page load instead of scroll?
Yes. The demos with a Replay button run on load; remove the button for a one-off entrance.
Which reveal is cheapest?
clip-path and transform reveals are all compositor friendly. Blur is a little heavier on large images.