Cinematic Image Wipe
Full-viewport frames revealed by a directional clip-path wipe that alternates side to side, captions riding in a beat behind.
Cinematic Image Wipe the 11th of 12 designs in the 12 CSS Scroll Animations collection. The design pairs CSS styling with a small amount of JavaScript for interactivity. Copy the HTML, CSS and JavaScript panels below into your project — the JS is self-contained, has zero dependencies, and is safe to drop into any framework (React, Vue, Svelte, plain HTML). The design honours prefers-reduced-motion and uses real semantic markup, so it ships accessibility-ready out of the box.
Live preview
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="intro">
<div class="tag">A Cinematic Sequence</div>
<h1>Frames<br>That Wipe In</h1>
</div>
<section class="frame">
<div class="media m1"></div>
<div class="cap">
<div class="no">Reel 01</div>
<h2>Dusk Over the Ridge</h2>
<p>The first frame slides in from the left, edge sweeping across the image like a curtain pulled fast.</p>
</div>
</section>
<section class="frame">
<div class="media m2"></div>
<div class="cap">
<div class="no">Reel 02</div>
<h2>Cold Harbour Light</h2>
<p>The wipe reverses — entering from the right, the direction alternating frame to frame.</p>
</div>
</section>
<section class="frame">
<div class="media m3"></div>
<div class="cap">
<div class="no">Reel 03</div>
<h2>Violet Hour Interior</h2>
<p>A long easing curve gives the reveal weight, the way a film cut lands rather than snaps.</p>
</div>
</section>
<section class="frame">
<div class="media m4"></div>
<div class="cap">
<div class="no">Reel 04</div>
<h2>Embers, After</h2>
<p>The caption rides in a beat behind the wipe — image first, words second.</p>
</div>
</section>
<div class="end">— End of Sequence —</div> *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{background:#070707;font-family:'Archivo',sans-serif;color:#fff;overflow-x:hidden}
.intro{
height:60vh;display:flex;flex-direction:column;
align-items:center;justify-content:center;text-align:center;gap:1rem;
}
.intro .tag{
font-size:11px;letter-spacing:0.3em;text-transform:uppercase;
color:#666;
}
.intro h1{
font-family:'Anton',sans-serif;font-size:clamp(40px,8vw,110px);
line-height:0.95;letter-spacing:0.01em;text-transform:uppercase;
}
/* each frame is full-viewport, image revealed by a wipe */
.frame{
position:relative;height:118vh;
display:flex;align-items:center;justify-content:center;
overflow:hidden;
}
.media{
position:absolute;inset:0;
/* clip starts collapsed; widens on .in */
clip-path:inset(0 100% 0 0);
transition:clip-path 1.35s cubic-bezier(0.76,0,0.24,1);
}
.frame.in .media{clip-path:inset(0 0 0 0)}
.frame:nth-child(even) .media{clip-path:inset(0 0 0 100%)}
.frame:nth-child(even).in .media{clip-path:inset(0 0 0 0)}
/* atmospheric "photographs" */
.m1{background:linear-gradient(135deg,#3A1C71,#D76D77 55%,#FFAF7B)}
.m2{background:linear-gradient(135deg,#0F2027,#203A43 55%,#2C5364)}
.m3{background:linear-gradient(135deg,#42275A,#734B6D)}
.m4{background:linear-gradient(135deg,#1F1C18,#8E0E00 60%,#1F1C18)}
.media::after{
content:'';position:absolute;inset:0;
background:radial-gradient(ellipse at center,transparent 30%,rgba(0,0,0,0.55))
}
/* caption rides in behind the wipe */
.cap{
position:relative;z-index:2;text-align:center;
opacity:0;transform:translateY(28px);
transition:opacity 0.8s ease 0.6s,transform 0.8s ease 0.6s;
}
.frame.in .cap{opacity:1;transform:none}
.cap .no{
font-family:'Anton',sans-serif;font-size:14px;
letter-spacing:0.2em;color:rgba(255,255,255,0.55);margin-bottom:0.6rem;
}
.cap h2{
font-family:'Anton',sans-serif;text-transform:uppercase;
font-size:clamp(34px,6vw,84px);line-height:1;letter-spacing:0.01em;
}
.cap p{
margin-top:1rem;font-size:14px;color:rgba(255,255,255,0.7);
max-width:34ch;margin-left:auto;margin-right:auto;line-height:1.6;
}
.end{
height:50vh;display:flex;align-items:center;justify-content:center;
font-family:'Anton',sans-serif;font-size:clamp(24px,4vw,52px);
text-transform:uppercase;color:#1F1F1F;
} const io=new IntersectionObserver((entries)=>{
entries.forEach(e=>{
if(!e.isIntersecting)return;
e.target.classList.add('in');
io.unobserve(e.target);
});
},{threshold:0.35});
document.querySelectorAll('.frame').forEach(el=>io.observe(el));