All components

Counter Curtain Hero Reveal

GSAP animation component · Published 2026-09-05 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Load sequence: a dark preloader counts 000 to 100 while a headline word rotates through five values, a light line advances and a small glass frame cycles through ten photos as it travels to its slot in the hero. The preloader text blurs out, the curtain wipes upward on a custom 'hop' ease, the hero words slide in from their masks and the frame expands with GSAP Flip until it becomes the section background under difference-blended type.

How it's built

Categorypreloader
Techgsap
GSAP pluginsCustomEase, SplitText, Flip
Complexitypage
Performance costmedium
Mobile-safeyes

preloader counter clip-path splittext flip custom-ease hero mix-blend

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Counter Curtain Hero Reveal — Load Counter, Rotating Word, Clip-Path Curtain & Flip-Expanding Frame

Goal

Build a landing-page intro. A dark preloader shows the brand name top-left and a rotating word top-right (Studios → Season → Chamber → Archive → Vision), a three-digit counter bottom-left that runs 000 → 100, a caption bottom-right and a thin iridescent progress line. During the same 3 seconds a small glass photo frame in the hero (hidden under the preloader) cycles through ten photos and travels from the section's left edge to its slot inside the second headline row. Then: the preloader text blurs and fades, the panel wipes upward with a custom "hop" ease, the three headline rows slide their words in from their masks (row 1 and 3 from the left, row 2 from the right), the caption and nav fade/slide in, and the frame expands with GSAP Flip until it fills the whole section behind the type, which is mix-blend-mode: difference so it stays readable on any photo.

Tech

Vanilla HTML/CSS/JS with ES module imports. gsap (npm) with CustomEase, SplitText and Flip. Font: Hanken Grotesk (500).

Layout / HTML

<div class="cc">
  <div class="cc-preloader" aria-hidden="true">
    <div class="cc-preloader__header">
      <div class="cc-preloader__row"><h1>Underlume</h1></div>
      <div class="cc-preloader__row"><h1 class="cc-preloader__word">Studios</h1></div>
    </div>
    <div class="cc-preloader__footer">
      <div class="cc-preloader__counter"><h1>000</h1></div>
      <div class="cc-preloader__copy"><p>Currently developing</p></div>
    </div>
    <span class="cc-preloader__bar"></span>
  </div>

  <nav class="cc-nav"><p>Underlume</p><p>Menu</p></nav>

  <section class="cc-hero">
    <div class="cc-hero__header">
      <div class="cc-hero__row"><h1>Everything</h1></div>
      <div class="cc-hero__row">
        <h1>Beneath</h1>
        <div class="cc-hero__frame"><!-- ten <img> --></div>
      </div>
      <div class="cc-hero__row"><h1>The surface</h1></div>
    </div>
    <div class="cc-hero__footer"><p>Seen and unseen</p></div>
  </section>
</div>

Styling

:root: --cc-ink:#f3f2ee, --cc-paper:#f3f2ee, --cc-preloader:#1b1f22, --cc-line:rgba(255,255,255,.22), --cc-frame-w:275px, --cc-frame-h:150px.

  • .ccmin-height:100svh; background:var(--cc-paper); font-family:"Hanken Grotesk". All h1, p: margin:0; text-transform:uppercase; font-weight:500; letter-spacing:-.02em; line-height:.85; h1 { font-size: clamp(2.5rem, 10vw, 15rem) }, p { font-size:.85rem; letter-spacing:.02em; line-height:1 }.
  • .cc-preloaderposition:fixed; inset:0; z-index:20; display:flex; flex-direction:column; justify-content:space-between; padding:2.5rem; color:var(--cc-ink); background: radial-gradient(120vmax 60vmax at 50% 120%, rgba(255,255,255,.08), transparent 60%), var(--cc-preloader); clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%); overflow:hidden. Second header row is justify-content:flex-end; footer is display:flex; justify-content:space-between; align-items:flex-end; counter uses font-variant-numeric:tabular-nums.
  • .cc-preloader__barposition:absolute; left:2.5rem; right:2.5rem; bottom:1.4rem; height:1px; background:linear-gradient(90deg, rgba(255,255,255,.9), rgba(178,196,255,.9), rgba(255,184,226,.8), rgba(160,255,228,.8)); transform:scaleX(0); transform-origin:left; opacity:.8.
  • .cc-navposition:fixed; inset:0 0 auto 0; z-index:5; display:flex; justify-content:space-between; padding:2.5rem; color:#fff; mix-blend-mode:difference.
  • .cc-heroposition:relative; height:100svh; padding:2.5rem; display:flex; align-items:flex-end; overflow:hidden; isolation:isolate; background:var(--cc-paper) (the background inside the isolated group is what makes difference work). .cc-hero__headerposition:relative; z-index:1; width:100%; display:flex; flex-direction:column. Rows: flex:1; display:flex; align-items:center; rows 1 and 3 justify-content:flex-end; row 2 gap:2.5rem. Row h1: color:#fff; mix-blend-mode:difference.
  • .cc-hero__frameposition:relative; flex-shrink:0; width:var(--cc-frame-w); height:var(--cc-frame-h); border-radius:10px; overflow:hidden; box-shadow:0 0 0 1px var(--cc-line), 0 30px 60px -30px rgba(0,0,0,.5); z-index:2. Its images are position:absolute; inset:0; object-fit:cover; opacity:0, .is-active { opacity:1 }. A ::after gradient (transparent 55% → rgba(0,0,0,.35)) fades in over 1s when the frame has .is-full.
  • .cc-hero__frame.is-fullposition:absolute; inset:0; width:auto; height:auto; border-radius:0; box-shadow:none; z-index:0.
  • .cc-hero__footerposition:absolute; left:2.5rem; bottom:2.5rem; z-index:1; its p is white, mix-blend-mode:difference, opacity:0.
  • .cc-word (SplitText word class) — position:relative.
  • @media (max-width:1000px): --cc-frame-w:100px; --cc-frame-h:56px, paddings 1.5rem, row-2 gap 1rem, footer bottom:25svh.

GSAP effect (be exact)

gsap.registerPlugin(CustomEase, SplitText, Flip); CustomEase.create("cc-hop", "0.8, 0, 0.1, 1"). Build in mount(config)destroy().

const DEFAULTS = { startDelay: .5, loadDuration: 3, curtainDuration: 1, wordDuration: 1.25, expandDuration: 1.25, imageLoops: 3 };
const ROTATING = ["Studios", "Season", "Chamber", "Archive", "Vision"];

Prep

  • Split each hero h1 with SplitText.create(h, { type:"words", mask:"words", wordsClass:"cc-word" }); set words of row 2 to xPercent: 100, rows 1 and 3 to xPercent: -100.
  • gsap.set(nav, { y: -300 }); preloader clipPath full, display:flex; the three fading blocks opacity:1, filter:"blur(0px)"; bar scaleX:0; footer opacity:0; first image .is-active; counter 000; word Studios.
  • Frame start offset: pad = parseFloat(getComputedStyle(hero).paddingLeft); leftOffset = pad + hero.getBoundingClientRect().left - frame.getBoundingClientRect().left; gsap.set(frame, { x: leftOffset }) — the frame begins flush with the section's left padding.

Renderers driven by plain objects:

  • counter.ptextContent = String(round(p)).padStart(3, "0").
  • wordCycle.pROTATING[round(p)] (only update when the index changes).
  • imageCycle.pround(p) % images.length toggles .is-active.

Expandstate = Flip.getState(frame); add .is-full; gsap.set(frame, { x: 0 }); Flip.from(state, { duration: expandDuration, ease: "cc-hop", absolute: true }).

Timeline (delay: startDelay, L = loadDuration):

  1. 0counter { p: 100, duration: L, ease: "none", onUpdate }.
  2. 0 — bar scaleX: 1, L, ease:"none".
  3. 0 — frame x: 0, L, ease:"none".
  4. 0wordCycle { p: ROTATING.length - 1, duration: L, ease:"none", onUpdate }.
  5. 0imageCycle { p: images.length * imageLoops - 1, duration: L, ease:"none", onUpdate } (three full laps over ten photos).
  6. "+=0.3" — header, counter and copy { opacity: 0, filter: "blur(10px)", duration: .35, stagger: .04 }; "<" bar opacity: 0, .2s.
  7. next — preloader clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", curtainDuration, ease: "cc-hop", onCompletedisplay:none.
  8. "-=0.5" — all words xPercent: 0, wordDuration, power3.out, onComplete: expand.
  9. "<" — footer opacity: 1, 1s, power3.out.
  10. "<" — nav y: 0, 1s, power3.out.

destroy() — kill timeline/tweens, revert splits, remove .is-full, clearProps on nav/preloader/bar/footer/frame/fading blocks, reset images/counter/word.

Assets / images

Ten landscape photos (≈16:9, ≥1200px — the last one becomes the full-section background, so it should be the strongest): a coherent set of landscapes and architecture in a muted palette. Photos 1–9 are only ever seen at 275×150 for a fraction of a second each.

Behavior notes

  • The whole thing is a one-shot intro of ≈7s; the preloader is position:fixed and removed from view at the end.
  • mix-blend-mode: difference on the headline needs a painted background inside the same stacking context (.cc-hero has isolation:isolate and its own background) — without it the text renders white on white.
  • Flip with absolute:true lets the frame leave the flex row without the row collapsing; the Beneath heading keeps its place because it precedes the frame.
  • Mobile shrinks the frame to 100×56 and the paddings; the sequence is otherwise identical.

Images

This component ships with 10 reference assets, served publicly. Use them as-is to reproduce the demo faithfully, then swap in your own — the layout expects the same aspect ratios.

https://motionprompts.dev/c/counter-curtain-hero-reveal/img1.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img10.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img2.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img3.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img4.jpg
https://motionprompts.dev/c/counter-curtain-hero-reveal/img5.jpg
… 4 more under https://motionprompts.dev/c/counter-curtain-hero-reveal/

They are hotlinkable for prototyping. For anything you ship, replace them: they are licensed for demonstration of this component, not for redistribution.

Using this outside its demo page

This component is written as a complete page — that is how the demo is meant to look. If you are dropping it into an existing project, or combining it with other components, these are the things it declares at document level and that you need to move or reconcile first.

  • Palette on :root--cc-ink, --cc-paper, --cc-preloader, --cc-line, --cc-frame-w, --cc-frame-h. These names are not namespaced and they collide: --ink is defined by 164 of the 219 components in this catalogue, --paper by 94, --muted by 80, each with different values — and they will also collide with whatever your own project defines. Move them onto the component's wrapper (.my-section { --ink: … }) or rename them with a prefix.
  • Rules on html, body — the demo owns the whole document, so these set the page background, typography and resets. Dropped into an existing project they restyle the entire page, not just this section. Re-target them at the component's wrapper before using it.
  • Full-screen overlay — a fixed element covers the viewport (a loader or transition). Only one may exist per page and it must remove itself when done. If your page already has one, keep that and drop this; otherwise the second silently hides the first.