Header condense on scrollWeb · App
Target
Implement the "Curtain image reveal" effect in vanilla HTML/CSS/JS. Context: A solid panel slides off an image while the image eases from a slight zoom to rest, the first time it enters view. Trigger: the element enters the viewport (first time only). Timeline: time-based (duration + easing). Motion tokens (normal): - translateX (curtain): 0 → 101% - scale (image): 1.15 → 1 - duration: 900ms - easing: cubic-bezier(0.77, 0, 0.18, 1) - note: curtain and image run in parallel; image eases out over the same 900ms Motion tokens (prefers-reduced-motion): - opacity (image): 0 → 1 - duration: 250ms - easing: ease-out - note: curtain hidden Technique: prefer pure CSS transitions/animations; use JS only to toggle classes. Acceptance: - animate only transform and opacity unless the spec lists other properties - no layout shift; content is visible without the animation - when prefers-reduced-motion is set, use the reduced tokens only - frame clips overflow; neither the curtain nor the zoom affects layout - plays once; replay only on explicit request
Generated from the spec. Prompts differ by target name only; the tokens are the source of truth.
demo.html
<div class="fx-curtain-image-reveal" data-fx-root>
<figure class="fx-frame">
<div class="fx-img" role="img" aria-label="Placeholder image"></div>
<div class="fx-curtain" aria-hidden="true"></div>
</figure>
</div>
demo.css
.fx-curtain-image-reveal { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.fx-curtain-image-reveal .fx-frame { position: relative; margin: 0; width: 62%; aspect-ratio: 16 / 10; max-height: 80%; border-radius: 12px; overflow: hidden; }
.fx-curtain-image-reveal .fx-img { width: 100%; height: 100%; background: linear-gradient(135deg, #fbcfe8, #a78bfa 50%, #4f46e5); transform: scale(1.15); }
.fx-curtain-image-reveal .fx-curtain { position: absolute; inset: 0; background: var(--fx-ink, #18181b); }
.fx-curtain-image-reveal.is-playing .fx-curtain { animation: fx-cir-curtain 900ms cubic-bezier(.77, 0, .18, 1) both; }
.fx-curtain-image-reveal.is-playing .fx-img { animation: fx-cir-img 900ms cubic-bezier(.77, 0, .18, 1) both; }
@keyframes fx-cir-curtain { to { transform: translateX(101%); } }
@keyframes fx-cir-img { to { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
.fx-curtain-image-reveal .fx-curtain { display: none; }
.fx-curtain-image-reveal .fx-img { transform: none; opacity: 0; }
.fx-curtain-image-reveal.is-playing .fx-img { animation: fx-cir-fade 250ms ease-out both; }
@keyframes fx-cir-fade { to { opacity: 1; } }
}
demo.js
export default function mount(root) {
return {
play() { root.classList.remove('is-playing'); void root.offsetWidth; root.classList.add('is-playing'); },
stop() { root.classList.remove('is-playing'); },
};
}
Inspired by tympanus.net — a staple of Codrops image reveal demos. The preview is an original re-creation; the effect name belongs to its author.
Both layers use the same asymmetric curve (`.77,0,.18,1`), which reads as "heavy fabric". Translate the curtain 101% rather than 100% to avoid a 1px seam from subpixel rounding.
Added 2026-11-09 · Report an issue