All components

Tile Grid Preloader Reveal

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

Open live demo ↗ Raw prompt (.md)

What it does

Load sequence: an odd-numbered grid of frosted glass tiles fills in random order while a progress marker hops with GSAP Flip across four stops on the middle row (25, 50, 75, then the logo). The veil then drops away and every tile folds on its top edge in random order, briefly blurring the hero photo underneath, before the headline characters, subtitle lines and nav words rise from their masks.

How it's built

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

preloader grid tiles flip splittext glass reveal random-stagger

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

Tile Grid Preloader Reveal — Glass Tiles, Flip-Hopping Progress Marker & Masked Hero Text

Goal

Build a landing-page load sequence. A pale veil covers the page and a grid of frosted glass tiles (always an odd number of rows and columns, plus one hidden ring outside the viewport) fills in random order. A dark square progress marker sits on the middle row and hops with GSAP Flip across four stops — showing 25, 50, 75, then a small logo — while the stops it leaves turn back into glass. When the marker lands in the centre, the veil is removed, and every tile folds down on its top edge (scaleY → 0, random order), briefly blurring the hero photo underneath through the glass; the hero then settles from a slight zoom, and the headline characters, subtitle lines and nav words rise out of their masks.

Tech

Vanilla HTML/CSS/JS with ES module imports. gsap (npm) plus the plugins Flip and SplitText. Fonts: Barlow Condensed (800), DM Mono (500), DM Sans (500–600).

Layout / HTML

<div class="tg">
  <div class="tg-preloader" aria-hidden="true">
    <div class="tg-preloader__bg"></div>
    <div class="tg-preloader__grid"></div>
    <div class="tg-marker"><span class="tg-marker__value">25</span></div>
  </div>

  <nav class="tg-nav">
    <a class="tg-brand" href="#">Frme</a>
    <div class="tg-links"><a href="#">Work</a><a href="#">Studio</a><a href="#">Process</a><a href="#">Contact</a></div>
  </nav>

  <section class="tg-hero">
    <div class="tg-hero__bg"><img src="…" alt="" /></div>
    <div class="tg-hero__title"><h1>Frme</h1></div>
    <div class="tg-hero__subtitle"><h3>Shaping the world of digital expression</h3></div>
  </section>
</div>

Styling

:root variables: --tg-ink:#f4f3ef, --tg-veil:#e9e8e3, --tg-tile:rgba(255,255,255,.55), --tg-tile-line:rgba(20,22,27,.08), --tg-marker:#14161b, --tg-marker-ink:#f4f3ef, --tg-accent:#c4d600.

  • .tgmin-height:100svh; background:#0f1114; color:var(--tg-ink); font-family:"DM Sans".
  • .tg-preloaderposition:fixed; inset:0; z-index:20; overflow:hidden. .tg-preloader__bgposition:absolute; inset:0; background:var(--tg-veil). .tg-preloader__gridposition:absolute; top:50%; left:50%; transform:translate(-50%,-50%); display:flex; flex-wrap:wrap; z-index:1 (JS sets its width/height).
  • .tg-tileposition:relative; background:var(--tg-tile); backdrop-filter:blur(10px) saturate(1.2); outline:1px solid var(--tg-tile-line); outline-offset:-.5px; ::after is a soft glass sheen linear-gradient(160deg, rgba(255,255,255,.35), transparent 55%). .tg-tile.is-stopbackground:var(--tg-marker); outline-color:var(--tg-marker) with the sheen at 12% opacity.
  • .tg-markerposition:absolute; top:0; left:0; z-index:2; display:flex; align-items:center; justify-content:center; background:var(--tg-marker); color:var(--tg-marker-ink); font:500 .85rem "DM Mono"; font-variant-numeric:tabular-nums; box-shadow:0 18px 40px -18px rgba(0,0,0,.55); an inline svg inside it is 46% wide/high.
  • .tg-nav — fixed, z-index:5; padding:2rem 2.5rem; display:flex; justify-content:space-between; align-items:flex-start; links .95rem weight 500, brand 600; .tg-links { display:flex; gap:2.5rem }.
  • .tg-heroheight:100svh; padding:2.5rem; display:flex; justify-content:space-between; align-items:flex-end; overflow:hidden. .tg-hero__bg absolute inset:0 with a ::after gradient rgba(0,0,0,.1) → rgba(0,0,0,.45); img object-fit:cover.
  • h1color:var(--tg-accent); font:800 clamp(4rem,20vw,24rem) "Barlow Condensed"; letter-spacing:-.02em; text-transform:uppercase; line-height:.8. h3width:20rem; font-weight:500; font-size:clamp(1.4rem,2.6vw,3rem); line-height:1.2.
  • @media (max-width:1000px): nav padding 1.5rem, links stacked right-aligned, hero as a column with align-items:flex-start.

GSAP effect (be exact)

gsap.registerPlugin(Flip, SplitText). Everything in mount(config)destroy().

const DEFAULTS = { startDelay: .6, fillDuration: 2.5, hopDuration: 1, collapseDuration: .75, collapseStagger: .0035, titleStagger: .05, tileSize: 85 };

Grid build

  • isDesktop = innerWidth >= 1000; maxTile = isDesktop ? tileSize : round(tileSize * .6).
  • snapToOdd(v) = v % 2 === 0 ? v - 1 : v; cols = max(3, snapToOdd(floor(innerWidth / maxTile))), rows = max(3, snapToOdd(floor(innerHeight / maxTile))).
  • tile = min(innerWidth / cols, innerHeight / rows); totalCols = cols + 2, totalRows = rows + 2 (one hidden ring around the viewport so the centred grid always overflows the edges).
  • Grid element sized totalCols*tile × totalRows*tile; create totalCols*totalRows .tg-tile divs of tile × tile.
  • midRow = floor(totalRows/2), midCol = floor(totalCols/2), at(col) = tiles[midRow*totalCols + col].
  • Stops: reach = isDesktop ? [5,3,5] : [2,1,2]stops = [at(midCol - reach[0]), at(midCol + reach[1]), at(midCol + reach[2]), at(midCol)], each gets .is-stop.
  • fillTiles = tiles not in stopsgsap.set(fillTiles, { opacity: 0 }).
  • Marker: gsap.set(marker, { width: tile, height: tile }) and position it on the first stop via getBoundingClientRect() offsets relative to the preloader (left = tileRect.left - preloaderRect.left, same for top).

Marker hophopTo(tile, html): state = Flip.getState(marker); gsap.set(marker, offsetOf(tile)); replace the marker's inner HTML; Flip.from(state, { duration: hopDuration, ease: "power2.inOut" }).

Text prepSplitText.create(".tg-hero__title h1", { type:"chars", mask:"chars" }), …h3 { type:"lines", mask:"lines" }, ".tg-nav a" { type:"words", mask:"words" }; gsap.set([chars, lines, words], { yPercent: 110 }); gsap.set(heroBg, { scale: 1.12 }).

Timeline (delay: startDelay):

  1. 0fillTiles opacity: 1, duration .125, stagger: { each: fillDuration / fillTiles.length, from: "random" }.
  2. 0.25 — callback hopTo(stops[1], "<span class='tg-marker__value'>50</span>").
  3. 0.25 + hop*1.25hopTo(stops[2], "75").
  4. 0.25 + hop*2.5hopTo(stops[3], LOGO_SVG) and, at the same time, remove .is-stop from the first three stops (they melt back into the glass grid). LOGO_SVG is a 24×24 stroke icon (a simple "F": M4 20V4h13 + M4 12h9, stroke-width 2.2, round caps).
  5. ">${hop}" — callback: hide .tg-preloader__bg (display:none) — now the glass tiles blur the hero beneath.
  6. "+=0.35"collapsing = tiles except stops[3]{ scaleY: 0, transformOrigin: "top", duration: collapseDuration, ease: "power3.out", stagger: { each: collapseStagger, from: "random" } }.
  7. "<"[stops[3], marker] same scaleY: 0 collapse (no stagger).
  8. "<" — hero background scale: 1, 2.2s, power3.out.
  9. callback — hide the whole preloader (display:none).
  10. "<0.2" — title chars yPercent: 0, 1s, power3.out, stagger: { each: titleStagger, from: "random" }.
  11. "<0.5" — subtitle lines yPercent: 0, 1s, stagger: .1.
  12. "<0.25" — nav words yPercent: 0, 1s, stagger: .075.

destroy() — kill the timeline and tweens, revert() the three splits, empty the grid, reset the marker's style/HTML, clearProps on the hero background, restore the preloader/veil display.

Assets / images

One calm landscape hero (≈16:9, ≥1600px): a misty lake, a field, a coastline. It is seen through frosted tiles for a moment, so it should have soft large shapes rather than fine detail. The logo is an inline SVG (no asset).

Behavior notes

  • The sequence takes ≈7s at defaults and runs once per page load; it is a page-level intro (position:fixed veil).
  • Grid maths runs at mount; on resize mid-sequence nothing recalculates (the preloader is gone in seconds).
  • The tiles use backdrop-filter; if you need to support browsers without it, the collapse still reads fine — the blur is a bonus, not the mechanism.
  • Keep the prefixed variables on :root for theming (--tg-accent, --tg-veil, --tg-marker).

Images

This component ships with 1 reference asset, 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/tile-grid-preloader-reveal/hero.jpg

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--tg-ink, --tg-veil, --tg-tile, --tg-tile-line, --tg-marker, --tg-marker-ink, --tg-accent. 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.