All components

Sci-Fi Preloader Button Hero Reveal

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

Open live demo ↗ Raw prompt (.md)

What it does

Terminal-styled preloader that auto-plays on load: monospace status lines rise with masked SplitText, a circular SVG button outline draws itself while rotating 270deg, and a progress stroke fills in randomized stops before the logo swaps to an 'Engage' label. Clicking the button scales the preloader down, unwinds the strokes, flips the label to 'Access Granted' and wipes preloader plus white revealer left via clip-path, scaling the hero up as its headline words stagger in.

How it's built

Categorypreloader
Techgsap
GSAP pluginsSplitText, CustomEase
Complexitypage
Performance costmedium
Mobile-safeyes

preloader reveal clip-path splittext custom-ease svg-stroke hero click landing intro

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

Sci-Fi Terminal Preloader Button → Hero Clip-Path Reveal

Goal

Build a self-playing terminal-styled preloader that fills a circular SVG "loading" button, then waits for a click to wipe the whole preloader away and reveal a hero headline. On load: monospace status lines rise into view from behind masks (SplitText), a thin circular SVG outline draws itself while the whole ring rotates 270°, and a second progress stroke fills in randomized, jittery stages like a loader stalling and jumping. When it finishes, the centered logo fades out and an "Engage" label rises up — the button is now armed. Clicking the button scales the black preloader down, unwinds both SVG strokes off the ring, swaps the label from "Engage" up-and-out to "Access Granted" in, then wipes the black preloader and a white revealer panel leftward via clip-path while the hero scales up from 0.75 → 1 and its headline words stagger up out of masks. The star effect is the self-drawing + rotating + randomly-stepping SVG loader ring, followed by the synchronized left clip-path wipe → hero word reveal.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugins SplitText and CustomEase. No smooth-scroll library and no scroll interaction at all — the intro is a load-triggered timeline and the reveal is a click-triggered timeline. Imports:

import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
import CustomEase from "gsap/CustomEase";

Register once at module top: gsap.registerPlugin(SplitText, CustomEase). Immediately create two custom eases:

CustomEase.create("hop",   "0.9, 0, 0.1, 1");
CustomEase.create("glide", "0.8, 0, 0.2, 1");

All logic runs inside document.addEventListener("DOMContentLoaded", ...).

Layout / HTML

Three stacked fixed/relative layers, painted back-to-front by z-index:

  • .preloader-backdrop (fixed, full-viewport, white bg, gray text, z-index: 0) — a decorative terminal HUD sitting behind everything. Flex column, justify-content: space-between, so its content pins to top and bottom. It holds two .pb-rows (each display:flex; justify-content:space-between; padding:1.5rem):
  • Top .pb-row: five .pb-cols of monospace <p> lines — e.g. a repeated call-sign stacked 5×, a sector/id pair, a material/status pair, then a .pb-col containing #pb-logo (a small <img> with a dashed border), then a decorative glyph line.
  • Bottom .pb-row: six .pb-cols of short status <p> lines (single-word and two-line labels, a code like "F-9"), aligned to flex-end.
  • .preloader (fixed, full-viewport, black bg, white text, z-index: 2, overflow visible) — the active layer. Flex column, justify-content: space-between. Its initial clip-path is the full rectangle polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%). Contains:
  • .p-row with a single <p> "Initiating".
  • .p-row with a .p-col (two .p-sub-col groups of stacked <p> labels, gap: 6rem, aligned flex-end) and a second .p-col with a <p> code (e.g. "PX-17").
  • .preloader-btn-containerabsolutely centered, 20rem × 20rem, the click target. Inside, all absolutely centered on the same point:
  • #pbc-logo — a white <img> logo mark, 4rem × 4rem.
  • #pbc-label<p> "Engage".
  • #pbc-outro-label<p> "Access Granted".
  • .pbc-svg-strokes — an <svg viewBox="0 0 320 320"> containing two concentric <circle>s, cx=160 cy=160 r=155 stroke-width=2:
  • .stroke-track — dark stroke (#2b2b2b).
  • .stroke-progress — white stroke (#fff).

Both start with stroke-dasharray and stroke-dashoffset equal to the circumference (≈974) so nothing is drawn.

  • .hero (relative, full-viewport, black bg, white text, z-index below preloader) — flex-centered, text-align:center, and starts transform: scale(0.75). Contains:
  • .preloader-revealer — an absolutely positioned white panel covering the hero, with initial clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (full rectangle → hero hidden behind white).
  • <h1> headline — e.g. "The system is now visible", width 90%.

Keep all copy neutral/fictional sci-fi terminal text (call-signs, phase labels, "Engage", "Access Granted"). No real brand or client names.

Styling

Google Fonts: Barlow Condensed (weights 100–900, italics available) and Geist Mono (100–900).

Color tokens:

  • --base-100: #fff (white)
  • --base-200: #7a7a7a (mid gray)
  • --base-300: #000 (black)

Type:

  • h1: Barlow Condensed, uppercase, font-size: clamp(5rem, 15vw, 15rem), weight 800, letter-spacing: -2%, line-height: 0.8.
  • p: Geist Mono, uppercase, font-size: 0.75rem, weight 500, line-height: 1 (button labels #pbc-label/#pbc-outro-label are 0.9rem).
  • .preloader-backdrop is white bg + --base-200 gray text; .preloader and .hero are black bg + white text.

Split-piece initial state (load-bearing for the masked reveals):

h1 .word,
p  .line {
  position: relative;
  transform: translateY(100%);   /* every split line/word starts one line below */
  will-change: transform;
}

Key positioning / will-change (the animation depends on these):

  • .preloaderwill-change: transform, clip-path.
  • .preloader-btn-containerposition:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:20rem; height:20rem.
  • .pbc-svg-strokes, #pbc-logo, #pbc-label, #pbc-outro-label → all position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) (perfectly stacked). .pbc-svg-strokes and its svg are width:100%; height:100%; will-change:transform.
  • #pbc-logo4rem × 4rem. #pb-logo2.5rem × 2.5rem, padding:0.25rem, border:1px dashed var(--base-200).
  • .herotransform: scale(0.75); will-change:transform.
  • .hero .preloader-revealerposition:absolute; inset:0; width:100%; height:100%; background:var(--base-100); will-change:clip-path.

Responsive @media (max-width:1000px): hide the top .pb-row's 1st, 2nd, and 5th .pb-cols (display:none) to declutter the HUD on narrow screens. Nothing else about the animation changes.

GSAP effect (exhaustive)

Setup (once, inside DOMContentLoaded)
let preloaderComplete = false;

const preloaderTexts   = document.querySelectorAll(".preloader p");
const preloaderBtn     = document.querySelector(".preloader-btn-container");
const btnOutlineTrack  = document.querySelector(".stroke-track");
const btnOutlineProgress = document.querySelector(".stroke-progress");
const svgPathLength    = btnOutlineTrack.getTotalLength();   // ≈ 973.9 for r=155
  • gsap.set([btnOutlineTrack, btnOutlineProgress], { strokeDasharray: svgPathLength, strokeDashoffset: svgPathLength }) — normalize both circles to the measured circumference and hide them (nothing drawn).
  • SplitText: for each .preloader p, new SplitText(p, { type: "lines", linesClass: "line", mask: "lines" }) (masked lines). For the hero, new SplitText(".hero h1", { type: "words", wordsClass: "word", mask: "words" }) (masked words). The mask option wraps each piece in a clipped element so the translateY(100%) start state hides it fully.
Intro timeline (auto-plays on load)

const introTl = gsap.timeline({ delay: 1 }); — a 1s initial delay, then:

  1. Status lines rise. .to(".preloader .p-row p .line", { y: "0%", duration: 0.75, ease: "power3.out", stagger: 0.1 }) — every masked line inside the two .p-rows slides up into view, 0.1s apart. (Only .p-row lines here — the button labels are split too but animated later.)
  2. Track draws itself. .to(btnOutlineTrack, { strokeDashoffset: 0, duration: 2, ease: "hop" }, "<") — position "<" = starts together with step 1. The dark ring outline draws from nothing to a full circle over 2s.
  3. Whole ring rotates. .to(".pbc-svg-strokes svg", { rotation: 270, duration: 2, ease: "hop" }, "<") — also starts with step 1; the SVG spins 0° → 270° while it draws, so the stroke appears to sweep around.
  4. Randomized progress fill. Build stops:

``js const progressStops = [0.2, 0.25, 0.85, 1].map((base, i) => i === 3 ? 1 : base + (Math.random() - 0.5) * 0.1); // last is exactly 1; others jitter ±0.05 ` For each stop (index i), append: `js introTl.to(btnOutlineProgress, { strokeDashoffset: svgPathLength - svgPathLength * stop, // fills to stop fraction duration: 0.75, ease: "glide", delay: i === 0 ? 0.3 : 0.3 + Math.random() * 0.2, // stalls between jumps }); `` These are sequential (default end-of-timeline position). Net feel: the white progress stroke jumps to ~20%, pauses, nudges to ~25%, pauses, leaps to ~85%, then snaps to 100% — a stuttering, "real loader" fill.

  1. Logo fades out. .to("#pbc-logo", { opacity: 0, duration: 0.35, ease: "power1.out" }, "-=0.25") — overlaps back 0.25s into the last progress jump.
  2. Button shrinks. .to(preloaderBtn, { scale: 0.9, duration: 1.5, ease: "hop" }, "-=0.5") — the whole button container eases from scale 1 → 0.9.
  3. "Engage" label rises + arm the button. .to("#pbc-label .line", { y: "0%", duration: 0.75, ease: "power3.out", onComplete: () => { preloaderComplete = true; } }, "-=0.75") — the masked "Engage" text slides up where the logo was, and its onComplete flips preloaderComplete = true so the click handler will now fire.
Exit timeline (click .preloader-btn-container)

Listener on preloaderBtn: bail if !preloaderComplete; otherwise set preloaderComplete = false (one-shot) and run const exitTl = gsap.timeline();:

  1. Preloader scales down. .to(".preloader", { scale: 0.75, duration: 1.25, ease: "hop" }).
  2. Both strokes unwind off the ring. .to([btnOutlineTrack, btnOutlineProgress], { strokeDashoffset: -svgPathLength, duration: 1.25, ease: "hop" }, "<") — offset goes to negative circumference, so both circles animate out of view (unraveling), starting together with step 1.
  3. "Engage" exits up. .to("#pbc-label .line", { y: "-100%", duration: 0.75, ease: "power3.out" }, "-=1.25") — the current label slides up and out of its mask.
  4. "Access Granted" enters. .to("#pbc-outro-label .line", { y: "0%", duration: 0.75, ease: "power3.out" }, "-=0.75") — the outro label rises up into the same spot right behind it.
  5. Preloader wipes left. .to(".preloader", { clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)", duration: 1.5, ease: "hop" }) — the two right-hand corners collapse onto the left edge, so the black preloader wipes off to the left.
  6. Revealer wipes left (reveals hero). .to(".preloader-revealer", { clipPath: "polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)", duration: 1.5, ease: "hop", onComplete: () => gsap.set(".preloader", { display: "none" }) }, "-=1.45") — starts 1.45s before the previous ends, i.e. nearly in lockstep with the preloader wipe. The white revealer covering the hero collapses to the left too, uncovering the black hero + headline; on complete the now-empty preloader layer is hidden.
  7. Hero scales up. .to(".hero", { scale: 1, duration: 1.25, ease: "hop" }) — hero eases 0.75 → 1 (settles to full size as it's revealed).
  8. Headline words stagger in. .to(".hero h1 .word", { y: "0%", duration: 1, ease: "glide", stagger: 0.05 }, "-=1.75") — each masked word slides up into place 0.05s apart, overlapping back 1.75s so the words are rising while the hero is still scaling/revealing.
Eases used
  • hop = CustomEase "0.9, 0, 0.1, 1" — a hard, near-symmetric ease used for the ring draw/rotate, the scale changes, the stroke unwind, and the clip-path wipes.
  • glide = CustomEase "0.8, 0, 0.2, 1" — softer S-curve for the progress fill and the final headline words.
  • power3.out for the masked text rises, power1.out for the logo fade.

Assets / images

Two logo images (a third dark variant may exist but is unused by the component), all ~1:1 square PNGs on transparent background, each a small monochrome geometric emblem (e.g. a fragmented/split-hexagon glyph):

  1. Neutral/gray mark — used as #pb-logo, the tiny dashed-bordered badge in the white backdrop HUD (2.5rem).
  2. White mark — used as #pbc-logo, centered inside the circular button (4rem), and fades out mid-intro.

Provide simple, high-contrast marks; exact glyph is not important, aspect 1:1.

Behavior notes

  • Two triggers only: the intro is load-driven (1s delay, ~6s total), then it waits for a user click. The preloaderComplete boolean gates the click — the exit can't run until "Engage" has finished rising, and it's reset to false on click so the exit fires exactly once.
  • No ScrollTrigger, no Lenis, no scroll interaction anywhere.
  • The whole thing is percentage/clip-path/vw-based, so it scales fluidly; the ≤1000px media query only hides three decorative HUD columns.
  • Reduced-motion is not handled in the original — the animation always plays.