SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/image-reveal

7 CSS Image Reveal Animations

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.

01. Wipe Reveal on Scroll

RevealClip-path

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>
.ir1 { width: 270px; max-width: 100%; height: 290px; overflow-y: auto; padding: 14px; box-sizing: border-box; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font-family: Inter, system-ui, sans-serif; }
.ir1:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.ir1 p { height: 260px; margin: 0; color: #475467; font-size: 14px; }
.ir1 figure { margin: 0 0 20px; }
.ir1 .im { aspect-ratio: 4 / 3; border-radius: 12px; background: linear-gradient(160deg, #fdba74, #c2410c 60%, #7c2d12); }
.ir1 figcaption { margin-top: 6px; color: #475467; font-size: 13px; }
.ir1.js .im { clip-path: inset(0 100% 0 0); transition: clip-path 1s cubic-bezier(.7,0,.3,1); }
.ir1.js .in .im { clip-path: inset(0 0 0 0); }
@media (prefers-reduced-motion: reduce) { .ir1.js .im { clip-path: none; transition: none; } }
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>
.ir2 { position: relative; width: 260px; max-width: 100%; margin: 0; overflow: hidden; border-radius: 14px; font-family: Inter, system-ui, sans-serif; }
.ir2 .im { aspect-ratio: 4 / 3; background: linear-gradient(170deg, #93c5fd, #4ade80 55%, #166534); animation: ir2i .01s .55s both; }
.ir2 .blk { position: absolute; inset: 0; background: #facc15; transform: scaleX(0); transform-origin: left; animation: ir2b 1.1s cubic-bezier(.7,0,.3,1) both; }
@keyframes ir2b { 0% { transform: scaleX(0); transform-origin: left; } 50% { transform: scaleX(1); transform-origin: left; } 50.1% { transform-origin: right; } 100% { transform: scaleX(0); transform-origin: right; } }
@keyframes ir2i { from { opacity: 0; } }
.ir2 button { position: absolute; right: 8px; bottom: 8px; padding: 5px 10px; border: 0; border-radius: 6px; background: rgba(0,0,0,.6); color: #fff; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.ir2 button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .ir2 .blk, .ir2 .im { animation: none; } .ir2 .blk { display: none; } }
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>
.ir3 { position: relative; width: 260px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.ir3 .im { aspect-ratio: 4 / 3; border-radius: 14px; background: radial-gradient(circle at 40% 45%, #ccfbf1 0 12%, #2dd4bf 30%, #0e7490 70%); animation: ir3 1.1s cubic-bezier(.2,.8,.2,1) both; }
@keyframes ir3 { from { clip-path: circle(0 at 50% 50%); } to { clip-path: circle(75% at 50% 50%); } }
.ir3 button { margin-top: 8px; padding: 6px 12px; border: 1px solid #d0d5dd; border-radius: 6px; background: #fff; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.ir3 button:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .ir3 .im { animation: none; } }
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.

View code
<div class="ir4"><div class="im" role="img" aria-label="City skyline at night"></div><div class="sl" aria-hidden="true"><i style="--i:0"></i><i style="--i:1"></i><i style="--i:2"></i><i style="--i:3"></i><i style="--i:4"></i></div><button>Replay</button></div>
.ir4 { position: relative; width: 260px; max-width: 100%; border-radius: 14px; overflow: hidden; font-family: Inter, system-ui, sans-serif; }
.ir4 .im { aspect-ratio: 4 / 3; background: linear-gradient(transparent 55%, #0f172a 55%), repeating-linear-gradient(90deg, #1e293b 0 22px, #334155 22px 30px, #1e293b 30px 48px), linear-gradient(#312e81, #be185d); }
.ir4 .sl { position: absolute; inset: 0; display: grid; grid-template-rows: repeat(5, 1fr); }
.ir4 i { background: #e2e8f0; transform-origin: top; animation: ir4 .6s cubic-bezier(.7,0,.3,1) both; animation-delay: calc(var(--i) * 90ms + .2s); }
@keyframes ir4 { to { transform: scaleY(0); } }
.ir4 button { position: absolute; right: 8px; bottom: 8px; padding: 5px 10px; border: 0; border-radius: 6px; background: rgba(255,255,255,.85); color: #0f172a; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.ir4 button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .ir4 .sl { display: none; } }
document.querySelector('.ir4 button').onclick = function () { document.querySelectorAll('.ir4 i').forEach(function (e) { e.style.animation = 'none'; void e.offsetWidth; e.style.animation = ''; }); };

05. Blur to Sharp on View

RevealFilter

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>
.ir5 { width: 260px; max-width: 100%; height: 280px; overflow-y: auto; padding: 14px; box-sizing: border-box; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font-family: Inter, system-ui, sans-serif; }
.ir5:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.ir5 p { height: 220px; margin: 0; color: #475467; font-size: 14px; } .ir5 .end { height: auto; margin-top: 14px; }
.ir5 .im { aspect-ratio: 4 / 3; border-radius: 12px; background: repeating-linear-gradient(100deg, #a78bfa 0 8px, #7c3aed 8px 14px, #c4b5fd 14px 20px), #7c3aed; }
@supports (animation-timeline: view()) { .ir5 .im { animation: ir5 linear both; animation-timeline: view(); animation-range: entry 10% cover 45%; } @keyframes ir5 { from { filter: blur(14px); transform: scale(1.08); } } }
@media (prefers-reduced-motion: reduce) { .ir5 .im { animation: none; } }

06. Diagonal Wipe

RevealPolygon

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.

View code
<div class="ir6"><div class="im" role="img" aria-label="Orange desert dunes"></div><button>Replay</button></div>
.ir6 { position: relative; width: 260px; max-width: 100%; font-family: Inter, system-ui, sans-serif; }
.ir6 .im { aspect-ratio: 4 / 3; border-radius: 14px; background: linear-gradient(165deg, #fed7aa 0 30%, #fb923c 30% 55%, #c2410c 55% 75%, #7c2d12 75%); animation: ir6 1.1s cubic-bezier(.7,0,.3,1) both; }
@keyframes ir6 { from { clip-path: polygon(0 0, 0 0, -30% 100%, -30% 100%); } to { clip-path: polygon(0 0, 130% 0, 100% 100%, -30% 100%); } }
.ir6 button { margin-top: 8px; padding: 6px 12px; border: 1px solid #3a4670; border-radius: 6px; background: #1b2238; color: #e2e8f0; font: 600 12px Inter, system-ui, sans-serif; cursor: pointer; }
.ir6 button:focus-visible { outline: 2px solid #818cf8; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .ir6 .im { animation: none; } }
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>
.ir7 { width: 260px; max-width: 100%; height: 290px; overflow-y: auto; padding: 14px; box-sizing: border-box; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font-family: Inter, system-ui, sans-serif; }
.ir7:focus-visible { outline: 3px solid #84adff; outline-offset: 2px; }
.ir7 p { height: 250px; margin: 0; color: #475467; font-size: 14px; }
.ir7 figure { margin: 0 0 24px; }
.ir7 .im { aspect-ratio: 1; border-radius: 50% 50% 14px 14px; background: radial-gradient(circle at 50% 40%, #93c5fd, #1d4ed8 60%, #1e3a8a); }
.ir7 figcaption { margin-top: 8px; color: #475467; font-size: 13px; } .ir7 b { color: #101828; }
.ir7.js figure > * { opacity: 0; transform: translateY(20px); }
.ir7.js .in > * { animation: ir7 .7s cubic-bezier(.2,.8,.2,1) forwards; } .ir7.js .in figcaption { animation-delay: .2s; }
@keyframes ir7 { to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .ir7.js figure > * { opacity: 1; transform: none; animation: none !important; } }
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.