All components

Konpo Landing Page Reveal

GSAP animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Auto-playing landing page intro: three vertical rolling counters tick to 100 while a background wipe scales up and 15 rounded thumbnails pop in from scale 0. On load complete, GSAP Flip animates every image from the top-left corner to the bottom-right with a staggered scale-punch, then masked SplitText lines rise into place for the nav, heading, side info and footer. Triggered entirely on DOMContentLoaded.

How it's built

Categorypreloader
Techgsap
GSAP pluginsFlip, SplitText
Complexitypage
Performance costmedium
Mobile-safeyes

reveal preloader flip splittext counter landing hero stagger editorial

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

Landing Page Reveal Animation (rolling counter + Flip image reveal + masked line text)

Goal

Build an auto-playing landing-page intro for a design studio: three vertical rolling digit columns count from 0 to 100 in the bottom-right corner while a lighter background panel wipes up from the bottom and 15 rounded photo thumbnails pop in (scale 0 → 1) stacked in the top-left corner. When the "load" finishes, the counter fades out and GSAP Flip animates every thumbnail from the top-left corner to the bottom-right corner with a staggered scale-punch (each image blows up to 2.5× mid-flight and settles back to 1). Finally, divider lines draw in and every piece of text (nav, giant heading, side info, footer) rises into view line-by-line through overflow masks via SplitText. Everything runs once on DOMContentLoaded — no scroll, no hover.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugins Flip and SplitText:

import gsap from "gsap";
import { Flip } from "gsap/Flip";
import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(Flip, SplitText);

No Lenis, no ScrollTrigger — this is a load-triggered, single-viewport hero.

Layout / HTML

One <section class="hero"> filling the viewport, containing (in this order):

<section class="hero">
  <div class="hero-bg"></div>

  <div class="counter">
    <div class="counter-1 digit"></div>
    <div class="counter-2 digit"></div>
    <div class="counter-3 digit"></div>
  </div>

  <div class="images-container">
    <!-- 15 of these -->
    <div class="img"><img src="..." alt="" /></div>
  </div>

  <nav>
    <div class="logo-name"><a href="#">Omno</a></div>
    <div class="nav-items">
      <div class="links">
        <a href="#">Portfolio</a>
        <p>/</p>
        <a href="#">About</a>
      </div>
      <div class="cta"><a href="#">Contact Us</a></div>
    </div>
    <div class="divider"></div>
  </nav>

  <div class="sidebar">
    <div class="logo"><img src="..." alt="" /></div>
    <div class="divider"></div>
  </div>

  <div class="header">
    <h1>Visual engineering for modern brands</h1>
  </div>

  <div class="site-info">
    <h2>A design team focused on brands websites, apps and products</h2>
    <div class="divider"></div>
    <div class="site-info-copy">
      <p>Award-winning creative studio</p>
      <p>Operating since 2019</p>
    </div>
  </div>

  <div class="hero-footer">
    <h2>Watch showreel</h2>
  </div>
</section>

The three .counter-* divs start empty — their digit children are generated by JS (see below).

Styling

Palette (CSS custom properties):

  • --bg: #f1efe7 — warm off-white page/hero background
  • --fg: #1f1f1f — near-black text
  • --loader-bg: #e0e0d8 — slightly darker grey-beige for the wipe panel
  • --stroke: rgba(0, 0, 0, 0.2) — divider lines

Typography: "PP Neue Montreal" (import via @import url("https://fonts.cdnfonts.com/css/pp-neue-montreal");), everything weight 500.

  • h1: 6rem, letter-spacing: -0.1rem, line-height: 1.1
  • h2: 1.75rem, letter-spacing: -0.02rem, line-height: 1.1
  • a, p: 1rem, line-height: 1, overflow: hidden, color --fg, no underline
  • .logo-name a: 1.5rem

Global reset (* { margin:0; padding:0; box-sizing:border-box }); all img are width/height: 100%; object-fit: cover.

Key positioning (this is what makes the effect work):

  • .hero: position: relative; width: 100vw; height: 100svh; background: var(--bg); overflow: hidden.
  • .hero-bg: absolutely fills the hero, background: var(--loader-bg),

transform-origin: bottom, initial transform: scaleY(0%) — it wipes upward.

  • .counter: `position: fixed; right: 3rem; bottom: 2rem; display: flex; height: 120px;

font-size: 120px; line-height: 150px; -webkit-text-stroke: 2px var(--fg); clip-path: polygon(0 0, 100% 0, 100% 120px, 0 120px)` — the clip-path is the mask that hides digits above/below the 120px window. The digits keep the inherited dark text color, so they render as solid near-black numerals thickened by the 2px stroke.

  • .counter-1, .counter-2, .counter-3: position: relative; top: -15px (optical centering

inside the clip window).

  • .num1offset1 { position: relative; right: -30px } and

.num1offset2 { position: relative; right: -15px } — kerning fixes for the narrow "1" glyph.

  • .images-container: absolutely fills the hero.
  • .images-container .img: `position: absolute; top: 1.5rem; left: 1.5rem; width: 20%;

aspect-ratio: 5/3; border-radius: 0.75rem; overflow: hidden` — all 15 stack in the same top-left spot.

  • .images-container .img.animate-out: `top: unset; left: unset; bottom: 1.5rem;

right: 1.5rem` — same size, anchored bottom-right. This class swap is what Flip animates.

  • nav: `position: relative; width: 100vw; height: 5rem;

padding: 1.5rem 1.5rem 1.5rem 7.5rem; display: flex; justify-content: space-between; align-items: center. .nav-items is flex with gap: 7.5rem; .links flex with gap: 0.5rem. Its .divider is absolute at the bottom, full width, height: 1px, transform-origin: left, initial transform: scaleX(0%)`.

  • .sidebar: absolute, top-left, width: 5rem; height: 100svh; padding-top: 1.5rem, flex

centered horizontally, items at the top. .sidebar .logo: width: 2rem; aspect-ratio: 1; transform: scale(0). .sidebar .divider: absolute at its right edge, width: 1px; height: 100svh; transform-origin: top; transform: scaleY(0%).

  • All .dividers share background-color: var(--stroke).
  • .header: absolute, top: 35%; left: 7.5rem; transform: translateY(-50%); width: 60%.
  • .site-info: absolute, right: 1.5rem; top: 60%; transform: translateY(-50%); width: 20%,

flex column gap: 1rem. Its .divider is width: 100%; height: 1px; transform-origin: left; scaleX(0%). .site-info-copy is a flex column with gap: 0.25rem.

  • .hero-footer: absolute, bottom: 1.5rem; left: 7.5rem.
  • SplitText mask styles: .line { overflow: hidden } and

.line span { position: relative; display: block; transform: translateY(125%); will-change: transform } — every line starts pushed below its mask.

GSAP effect (exhaustive)

Everything happens inside a single DOMContentLoaded listener. Order of setup, then one main timeline plus three independent counter tweens.

1. Text splitting (setup, before any animation)

For every h1, h2, p, a on the page, run SplitText.create(element, { type: "lines", linesClass: "line" }). Then, for each produced .line element, replace its innerHTML with <span>{its textContent}</span> — i.e. manually wrap each line's text in a <span>. Combined with the CSS above, every line of text is a <span> at translateY(125%) inside an overflow: hidden .line mask.

2. Counter digit columns (setup)

Build three vertical digit strips (each child is <div class="num">):

  • .counter-1 (hundreds): two nums — "0", then "1" with extra class num1offset1.
  • .counter-2 (tens): eleven nums for i = 0..10 — text is i, except i === 10 renders

"0"; the i === 1 num gets extra class num1offset2.

  • .counter-3 (units): thirty nums for i = 0..29 with text i % 10 (0–9 repeated three

times), plus one final "0" num — 31 total.

So the resting state reads "000" and the final state (each column translated to its last num) reads "100".

3. Counter roll tweens (independent gsap.to calls, fired immediately)

Helper animateCounter(counter, duration, delay = 0):

  • numHeight = counter.querySelector(".num").clientHeight
  • totalDistance = (numberOfNums - 1) * numHeight
  • gsap.to(counter, { y: -totalDistance, duration, delay, ease: "power2.inOut" })

Calls, in this order:

  • .counter-3 → duration 2.5, no delay
  • .counter-2 → duration 3, no delay
  • .counter-1 → duration 2, delay 1.5

All three columns land simultaneously at ~3.5s, reading 100.

4. Main timeline (const tl = gsap.timeline())

Before the timeline: gsap.set(".img", { scale: 0 }).

  1. tl.to(".hero-bg", { scaleY: "100%", duration: 3, ease: "power2.inOut", delay: 0.25 })

— the darker panel wipes up from the bottom (transform-origin bottom).

  1. tl.to(".img", { scale: 1, duration: 1, stagger: 0.125, ease: "power3.out" }, "<") — at

the same time, the 15 stacked thumbnails pop in one after another (15 × 0.125 ≈ 1.75s of stagger + 1s tween).

  1. `tl.to(".counter", { opacity: 0, duration: 0.3, ease: "power3.out", delay: 0.3,

onStart: () => animateImages() }) — after the wipe finishes (plus 0.3s), the counter fades out and the Flip sequence (below) is kicked off via onStart. Note animateImages() runs as its own timeline, not nested in tl`, so the following steps overlap it.

  1. `tl.to(".sidebar .divider", { scaleY: "100%", duration: 1, ease: "power3.inOut",

delay: 1.25 })` — vertical sidebar line draws downward.

  1. `tl.to(["nav .divider", ".site-info .divider"], { scaleX: "100%", duration: 1,

stagger: 0.5, ease: "power3.inOut" }, "<")` — horizontal lines draw from the left, 0.5s apart.

  1. tl.to(".logo", { scale: 1, duration: 1, ease: "power4.inOut" }, "<") — sidebar logo mark

scales 0 → 1.

  1. `tl.to([".logo-name a span", ".links a span, .links p span", ".cta a span"],

{ y: "0%", duration: 1, stagger: 0.1, ease: "power4.out", delay: 0.5 }, "<") — nav text lines rise out of their masks (from the CSS translateY(125%) to 0%`).

  1. `tl.to([".header span", ".site-info span", ".hero-footer span"],

{ y: "0%", duration: 1, stagger: 0.1, ease: "power4.out" }, "<") — heading, side info and footer lines rise, starting together with step 7 (both positioned at "<"`).

5. animateImages() — the Flip move (star of the show)
const images = document.querySelectorAll(".img");
images.forEach((img) => img.classList.remove("animate-out"));
const state = Flip.getState(images);
images.forEach((img) => img.classList.add("animate-out"));

const mainTimeline = gsap.timeline();
mainTimeline.add(
  Flip.from(state, { duration: 1, stagger: 0.1, ease: "power3.inOut" })
);

Then, for each image (index i), build a nested scale-punch timeline and add it to mainTimeline at position i * 0.1 (matching the Flip stagger so each punch rides its own flight):

  • .to(img, { scale: 2.5, duration: 0.45, ease: "power3.in" }, 0.025) — grows while leaving

the corner,

  • .to(img, { scale: 1, duration: 0.45, ease: "power3.out" }, 0.5) — shrinks back as it lands

bottom-right.

Net effect: images peel off the top-left stack one every 0.1s, each swelling to 2.5× at the midpoint of its 1s diagonal Flip journey and settling at 1× on the bottom-right stack.

Assets / images

  • 15 photographic thumbnails, aspect ratio 5:3 (landscape), varied editorial/portfolio

photography — they render as small rounded-corner cards (~20% viewport width). Cropping is fine (object-fit: cover).

  • 1 small square logo mark (1:1, ~32px render size), simple dark abstract glyph on

transparent or matching background, used in the left sidebar.

Use neutral placeholder text/branding — the demo studio name is "Omno".

Behavior notes

  • Runs exactly once per page load; no user input required and no scroll interaction.
  • Total sequence ≈ 5.5–6s from load to fully revealed layout.
  • Responsive: below 1000px wide — h1 drops to 2.5rem (letter-spacing -0.05rem), h2

to 1.5rem, nav .links is hidden, .img widens to 30%, .header moves to top: 25% with width: calc(100% - 12.5rem), and .site-info spans calc(100% - 12.5rem) anchored left: 7.5rem instead of right.

  • The counter digits rely on clientHeight of a rendered .num, so build the columns before

starting the tweens (same tick is fine — the elements just need to be in the DOM).

  • No reduced-motion handling is required; keep the hero overflow: hidden so flipped images

never spill.