ToolsWaves
Free GSAP Landing Page Template ยท ToolsWaves ยท Product

Free Product Showcase Hero Template with Masked-Letter Reveal

Free editorial product showcase landing page template from ToolsWaves with a massive masked-letter reveal animation, frosted glass card overlay, mouse parallax, vertical sticker, and video CTA. Free for any commercial use.

ProductEditorialParallaxFrosted GlassVideo CTA

About this ToolsWaves template

This free editorial product showcase template from ToolsWaves is built for premium product launches โ€” physical products, hardware releases, or limited-edition drops where the product photography is the hero. The page opens with a massive masked-letter reveal animation that introduces the product name with editorial weight. The composition uses a frosted glass card to overlay key product information, mouse parallax to give the scene depth, a vertical sticker for tagline emphasis, and a video CTA that invites visitors to see the product in motion.

Use this template for premium consumer electronics launches, designer goods, limited-release products, or any direct-to-consumer brand whose visual identity is its differentiator. ToolsWaves provides this template free with no licensing fees โ€” copy the HTML, CSS, and JavaScript files into your project, swap the placeholder product imagery for your actual product photography, and update the frosted glass overlay copy to match your value proposition. The mouse parallax depth can be tuned in the JavaScript file.

Copy the code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Free Product Showcase Hero Template with Masked-Letter Reveal</title>
  <meta name="description" content="Free GSAP product launch landing page hero from ToolsWaves" />
  <meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />

  <!-- Open Graph -->
  <meta property="og:title" content="Free Product Showcase Hero Template with Masked-Letter Reveal" />
  <meta property="og:description" content="Free GSAP product launch landing page hero from ToolsWaves" />
  <meta property="og:type" content="website" />
  <meta property="og:image" content="https://toolswaves.in/og?title=Free%20Product%20Showcase%20Hero%20Template%20with%20Masked-Letter%20Reveal&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Free Product Showcase Hero Template with Masked-Letter Reveal" />
  <meta name="twitter:description" content="Free GSAP product launch landing page hero from ToolsWaves" />
  <meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Product%20Showcase%20Hero%20Template%20with%20Masked-Letter%20Reveal&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Bootstrap (used by template โ€” replace with your own framework if you prefer) -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" />

  <style>
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
    --cream: #f2e8d5;
    --cream-soft: rgba(242, 232, 213, 0.85);
    --cream-dim: rgba(242, 232, 213, 0.55);
    --ink: #1a1a1a;
    --wood-1: #8a6538;
    --wood-2: #6b4a25;
    --wood-3: #5a3d1e;
    --mat-green-1: #3d5230;
    --mat-green-2: #2d3f23;
    --cork-1: #c49560;
    --cork-2: #8a6030;
    --cork-3: #6b4620;
    --orange: #e89a3c;
    --glass: rgba(242, 232, 213, 0.08);
    --glass-border: rgba(242, 232, 213, 0.18);
}

html, body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    background: #2b2a22;
    color: var(--cream);
    overflow: hidden;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

.back-link {
    position: fixed;
    bottom: 0.75rem;
    left: 0.75rem;
    z-index: 1000;
    color: var(--cream);
    text-decoration: none;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 0.4rem 0.75rem;
    background: rgba(26, 26, 26, 0.55);
    border: 1px solid rgba(242, 232, 213, 0.18);
    border-radius: 999px;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    transition: background 0.3s ease, opacity 0.3s ease;
    opacity: 0.7;
}
.back-link:hover { background: rgba(26, 26, 26, 0.85); opacity: 1; }

/* =========================================================
   LOADER
   ========================================================= */
.loader {
    position: fixed;
    inset: 0;
    z-index: 999;
    pointer-events: none;
}

.loader__panel {
    position: absolute;
    left: 0;
    right: 0;
    height: 50%;
    background: var(--cream);
    z-index: 1;
}
.loader__panel--top { top: 0; }
.loader__panel--bottom { bottom: 0; }

.loader__content {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2rem;
}

.loader__brand {
    width: min(420px, 70vw);
}
.loader__wordmark {
    width: 100%;
    height: auto;
    display: block;
}
.loader__text-stroke {
    stroke-dasharray: 1400;
    stroke-dashoffset: 1400;
}
.loader__text-fill {
    opacity: 0;
}

.loader__ruler {
    width: min(420px, 70vw);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}
.loader__ruler-track {
    position: relative;
    height: 28px;
    background: rgba(26, 26, 26, 0.08);
    border-radius: 2px;
    overflow: hidden;
}
.loader__ruler-fill {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    width: 0%;
    background: var(--ink);
}
.loader__ruler-ticks {
    position: absolute;
    inset: 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    padding: 0 2px;
    pointer-events: none;
}
.loader__ruler-ticks span {
    display: block;
    width: 1px;
    height: 14px;
    background: rgba(26, 26, 26, 0.35);
}
.loader__ruler-ticks span:nth-child(odd) { height: 8px; }
.loader__ruler-ticks span:nth-child(6) { height: 20px; }

.loader__ruler-meta {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.7rem;
    letter-spacing: 0.15em;
    color: var(--ink);
    text-transform: uppercase;
}
.loader__counter {
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* =========================================================
   SHOWCASE
   ========================================================= */
.showcase {
    position: relative;
    width: 100%;
    height: 100vh;
    min-height: 720px;
    overflow: hidden;
    background: var(--wood-3);
}

/* =========================================================
   SCENE LAYER (desk + mat + coaster + tools)
   ========================================================= */
.scene {
    position: absolute;
    inset: 0;
    z-index: 1;
    perspective: 1400px;
}
.scene > * {
    position: absolute;
}

/* --- Wooden desk --- */
.scene__desk {
    inset: -5%;
    background:
        repeating-linear-gradient(92deg,
            transparent 0,
            transparent 80px,
            rgba(0, 0, 0, 0.06) 80px,
            rgba(0, 0, 0, 0.06) 82px,
            transparent 82px,
            transparent 160px,
            rgba(255, 255, 255, 0.04) 160px,
            rgba(255, 255, 255, 0.04) 162px),
        repeating-linear-gradient(88deg,
            transparent 0,
            transparent 220px,
            rgba(0, 0, 0, 0.05) 220px,
            rgba(0, 0, 0, 0.05) 224px),
        radial-gradient(ellipse at 30% 50%, var(--wood-1) 0%, var(--wood-2) 40%, var(--wood-3) 100%);
    will-change: transform;
}

/* --- Cutting mat --- */
.scene__mat {
    top: 8%;
    left: 18%;
    width: 78%;
    height: 82%;
    transform: rotate(-1.5deg);
    background:
        linear-gradient(135deg, var(--mat-green-1) 0%, var(--mat-green-2) 100%);
    border-radius: 6px;
    box-shadow:
        0 30px 80px rgba(0, 0, 0, 0.5),
        0 8px 20px rgba(0, 0, 0, 0.3),
        inset 0 0 120px rgba(0, 0, 0, 0.25),
        inset 0 1px 0 rgba(255, 255, 255, 0.05);
    transform-origin: center;
    will-change: transform;
}
.scene__mat::before {
    /* Faint glow in the center */
    content: '';
    position: absolute;
    inset: 0;
    background: radial-gradient(ellipse at 50% 55%, rgba(220, 215, 180, 0.08) 0%, transparent 55%);
    border-radius: inherit;
    pointer-events: none;
}
.scene__mat-grid {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border-radius: inherit;
}

/* --- Cork coaster --- */
.scene__coaster {
    top: 50%;
    left: 50%;
    width: min(380px, 32vw);
    aspect-ratio: 1;
    transform: translate(-50%, -50%);
    will-change: transform;
}
.scene__coaster-rim {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background:
        radial-gradient(circle at 35% 30%, #d4a368 0%, var(--cork-2) 55%, var(--cork-3) 100%);
    box-shadow:
        0 30px 60px rgba(0, 0, 0, 0.55),
        0 10px 20px rgba(0, 0, 0, 0.35),
        inset 0 -12px 30px rgba(0, 0, 0, 0.4),
        inset 0 6px 15px rgba(255, 230, 180, 0.15);
}
.scene__coaster-rim::before {
    /* Cork speckle texture */
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background-image:
        radial-gradient(circle at 20% 30%, rgba(60, 30, 10, 0.4) 1px, transparent 2px),
        radial-gradient(circle at 60% 20%, rgba(60, 30, 10, 0.3) 1px, transparent 2px),
        radial-gradient(circle at 40% 60%, rgba(60, 30, 10, 0.35) 1px, transparent 2px),
        radial-gradient(circle at 80% 70%, rgba(60, 30, 10, 0.3) 1px, transparent 2px),
        radial-gradient(circle at 25% 80%, rgba(60, 30, 10, 0.4) 1px, transparent 2px),
        radial-gradient(circle at 70% 45%, rgba(60, 30, 10, 0.3) 1px, transparent 2px),
        radial-gradient(circle at 50% 15%, rgba(60, 30, 10, 0.35) 1px, transparent 2px);
    background-size: 40px 40px, 60px 60px, 50px 50px, 55px 55px, 45px 45px, 65px 65px, 35px 35px;
    opacity: 0.8;
    mix-blend-mode: multiply;
}
.scene__coaster-well {
    position: absolute;
    inset: 14%;
    border-radius: 50%;
    background:
        radial-gradient(circle at 40% 35%, #a77a40 0%, #7a5228 80%);
    box-shadow:
        inset 0 12px 25px rgba(0, 0, 0, 0.55),
        inset 0 -4px 10px rgba(255, 220, 160, 0.12);
}
.scene__coaster-well::after {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background-image:
        radial-gradient(circle at 15% 25%, rgba(40, 20, 5, 0.5) 1px, transparent 2px),
        radial-gradient(circle at 65% 35%, rgba(40, 20, 5, 0.4) 1px, transparent 2px),
        radial-gradient(circle at 40% 70%, rgba(40, 20, 5, 0.45) 1px, transparent 2px),
        radial-gradient(circle at 80% 60%, rgba(40, 20, 5, 0.4) 1px, transparent 2px),
        radial-gradient(circle at 25% 55%, rgba(40, 20, 5, 0.5) 1px, transparent 2px);
    background-size: 30px 30px, 45px 45px, 35px 35px, 50px 50px, 40px 40px;
    opacity: 0.75;
    mix-blend-mode: multiply;
}

/* --- Paperclips (left edge) --- */
.scene__paperclip {
    width: 70px;
    height: 120px;
    will-change: transform;
    filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.4));
}
.scene__paperclip svg { width: 100%; height: 100%; }
.scene__paperclip--1 {
    top: 18%;
    left: -10px;
    transform: rotate(-25deg);
}
.scene__paperclip--2 {
    top: 28%;
    left: 20px;
    transform: rotate(15deg);
    width: 60px;
    height: 110px;
}

/* --- Short pencil (bottom-left) --- */
.scene__pencil-short {
    top: 58%;
    left: -40px;
    width: 300px;
    height: 36px;
    transform: rotate(-18deg);
    will-change: transform;
    filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.45));
}
.scene__pencil-short svg { width: 100%; height: 100%; }

/* --- Long pencil (top-right, angled) --- */
.scene__pencil-long {
    top: 5%;
    right: -60px;
    width: 620px;
    height: 40px;
    transform: rotate(-68deg);
    transform-origin: right center;
    will-change: transform;
    filter: drop-shadow(-8px 10px 16px rgba(0, 0, 0, 0.5));
}
.scene__pencil-long svg { width: 100%; height: 100%; }

/* --- Box cutter (bottom-right) --- */
.scene__cutter {
    bottom: 10%;
    right: -20px;
    width: 360px;
    height: 82px;
    transform: rotate(18deg);
    will-change: transform;
    filter: drop-shadow(0 12px 18px rgba(0, 0, 0, 0.5));
}
.scene__cutter svg { width: 100%; height: 100%; }

/* --- Vignette --- */
.scene__vignette {
    inset: 0;
    background:
        radial-gradient(ellipse at center, transparent 35%, rgba(15, 12, 8, 0.6) 100%),
        linear-gradient(180deg, rgba(15, 12, 8, 0.25) 0%, transparent 25%, transparent 65%, rgba(15, 12, 8, 0.55) 100%);
    pointer-events: none;
    z-index: 10;
}

/* =========================================================
   UI LAYER
   ========================================================= */
.showcase > *:not(.scene) {
    position: absolute;
    z-index: 5;
}

/* Pager dot */
.showcase__pager { top: 2rem; left: 2rem; display: flex; gap: 0.5rem; }
.showcase__pager-dot {
    width: 10px; height: 10px; border-radius: 50%;
    background: var(--cream);
    box-shadow: 0 0 0 4px rgba(242, 232, 213, 0.15);
    animation: dotPulse 2.4s ease-in-out infinite;
}
@keyframes dotPulse {
    0%, 100% { box-shadow: 0 0 0 4px rgba(242, 232, 213, 0.15); }
    50% { box-shadow: 0 0 0 8px rgba(242, 232, 213, 0.05); }
}

/* Top tagline */
.showcase__tagline {
    top: 2rem; left: 50%;
    transform: translateX(-50%);
    font-size: 0.8rem; font-weight: 700;
    letter-spacing: 0.15em;
    color: var(--cream);
    text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
    white-space: nowrap;
}

/* Nav */
.showcase__nav { top: 2rem; right: 5rem; }
.showcase__nav ul { list-style: none; display: flex; gap: 2.5rem; }
.showcase__nav a {
    color: var(--cream); text-decoration: none;
    font-size: 0.8rem; font-weight: 700;
    letter-spacing: 0.15em;
    position: relative; padding-bottom: 0.25rem;
    transition: color 0.3s ease;
}
.showcase__nav a::after {
    content: ''; position: absolute;
    left: 0; bottom: 0; width: 100%; height: 1px;
    background: var(--cream);
    transform: scaleX(0); transform-origin: right;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.showcase__nav a.is-active::after,
.showcase__nav a:hover::after {
    transform: scaleX(1); transform-origin: left;
}

/* Vertical sticker */
.showcase__sticker {
    top: 1.5rem; right: 1.5rem;
    background: var(--cream); color: var(--ink);
    padding: 1rem 0.5rem;
    font-size: 0.7rem; font-weight: 700;
    letter-spacing: 0.2em;
    writing-mode: vertical-rl;
    box-shadow: -4px 6px 20px rgba(0, 0, 0, 0.3);
    border-radius: 2px;
}
.showcase__sticker span {
    display: inline-block;
    transform: rotate(180deg);
}

/* Title */
.showcase__title {
    top: 50%; left: 4rem;
    transform: translateY(-50%);
    font-family: 'Archivo Black', 'Inter', sans-serif;
    font-size: clamp(6rem, 18vw, 18rem);
    line-height: 0.85;
    letter-spacing: -0.04em;
    color: var(--cream);
    text-shadow: 0 4px 30px rgba(0, 0, 0, 0.35);
    display: flex; gap: 0.02em;
    pointer-events: none;
    will-change: transform;
}
.showcase__title-mask {
    display: inline-block; overflow: hidden;
    line-height: 1; padding-bottom: 0.05em;
}
.showcase__title-letter {
    display: inline-block;
    transform: translateY(110%);
    will-change: transform;
}

/* Description */
.showcase__description {
    top: 48%; right: 5rem;
    max-width: 420px;
    font-size: clamp(1rem, 1.3vw, 1.3rem);
    font-weight: 500; line-height: 1.4;
    color: var(--cream);
    text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
    will-change: transform;
}
.showcase__description .word-mask {
    display: inline-block; overflow: hidden;
    vertical-align: bottom;
}
.showcase__description .word {
    display: inline-block;
    transform: translateY(110%);
    will-change: transform;
}

/* Glass card */
.showcase__card {
    bottom: 3rem; left: 2rem;
    width: 320px;
    padding: 2rem 2rem 1.5rem;
    background: var(--glass);
    border: 1px solid var(--glass-border);
    border-radius: 4px;
    backdrop-filter: blur(16px) saturate(1.2);
    -webkit-backdrop-filter: blur(16px) saturate(1.2);
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
    will-change: transform;
}
.showcase__card-title {
    font-size: 1.25rem; font-weight: 800;
    line-height: 1.25; letter-spacing: 0.01em;
    color: var(--cream); margin-bottom: 1.5rem;
}
.showcase__card-divider {
    width: 100%; height: 1px;
    background: var(--cream-dim);
    margin-bottom: 1rem;
    transform-origin: left;
}
.showcase__card-sub {
    font-size: 0.95rem; font-weight: 500;
    line-height: 1.4; color: var(--cream-soft);
    text-align: right;
}

/* Scroll indicator */
.showcase__scroll {
    bottom: 2rem; left: 50%;
    transform: translateX(-50%);
    display: flex; flex-direction: column;
    align-items: center; gap: 0.5rem;
    color: var(--cream);
    font-size: 0.7rem; font-weight: 700;
    letter-spacing: 0.2em;
}
.showcase__scroll-icon {
    width: 32px; height: 32px;
    border: 1px solid var(--cream-dim);
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    animation: scrollBounce 2s ease-in-out infinite;
}
.showcase__scroll-icon svg { width: 14px; height: 14px; }
@keyframes scrollBounce {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(4px); }
}

/* Video thumb */
.showcase__video {
    bottom: 1.5rem; right: 1.5rem;
    width: 150px; height: 95px;
    border-radius: 4px; overflow: hidden;
    text-decoration: none; display: block;
    border: 2px solid var(--cream);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    transform-origin: bottom right;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.showcase__video:hover { transform: scale(1.05); }
.showcase__video-thumb {
    position: absolute; inset: 0;
    background:
        radial-gradient(ellipse at 30% 40%, #4a4a3e 0%, #1a1a14 100%);
    overflow: hidden;
}
.showcase__video-thumb img {
    width: 100%; height: 100%;
    object-fit: cover;
    filter: brightness(0.7) saturate(1.1);
    user-select: none;
}
.showcase__video-thumb::after {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, transparent 40%, rgba(26, 26, 26, 0.5) 100%);
    pointer-events: none;
}
.showcase__video-label {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    justify-content: space-between;
    padding: 0.5rem 0.75rem; z-index: 2;
}
.showcase__video-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: 1.5rem; line-height: 1;
    color: var(--cream); letter-spacing: -0.02em;
}
.showcase__video-play {
    align-self: flex-end;
    display: inline-flex; align-items: center; gap: 0.3rem;
    font-size: 0.65rem; font-weight: 700;
    letter-spacing: 0.15em; color: var(--cream);
}
.showcase__video-play svg { width: 10px; height: 10px; }

/* Initial entry states */
[data-fade], [data-nav], [data-sticker], [data-card], [data-video] { opacity: 0; }

/* Responsive */
@media (max-width: 1024px) {
    .showcase__nav { right: 4rem; }
    .showcase__nav ul { gap: 1.5rem; }
    .showcase__description { right: 2rem; top: auto; bottom: 12rem; max-width: 320px; }
    .showcase__title { left: 2rem; }
}
@media (max-width: 768px) {
    .showcase__tagline { font-size: 0.7rem; }
    .showcase__nav { display: none; }
    .showcase__description { bottom: 18rem; font-size: 0.95rem; }
    .showcase__card { width: calc(100% - 4rem); bottom: 9rem; }
    .showcase__video { width: 110px; height: 70px; }
    .showcase__scroll { display: none; }
    .scene__pencil-long { width: 420px; }
}

  </style>
</head>
<body>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Banner โ€” Product Showcase</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Archivo+Black&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="banner-product-showcase.css">
</head>
<body>
    

    <!-- ========== LOADER ========== -->
    <div class="loader" id="loader">
        <div class="loader__panel loader__panel--top"></div>
        <div class="loader__panel loader__panel--bottom"></div>
        <div class="loader__content">
            <div class="loader__brand">
                <svg class="loader__wordmark" viewBox="0 0 520 100" aria-hidden="true">
                    <text x="260" y="78" text-anchor="middle"
                          font-family="Archivo Black, sans-serif"
                          font-size="96" letter-spacing="-4"
                          fill="none"
                          stroke="#1a1a1a"
                          stroke-width="1.2"
                          class="loader__text-stroke">BRAND</text>
                    <text x="260" y="78" text-anchor="middle"
                          font-family="Archivo Black, sans-serif"
                          font-size="96" letter-spacing="-4"
                          fill="#1a1a1a"
                          class="loader__text-fill">BRAND</text>
                </svg>
            </div>
            <div class="loader__ruler">
                <div class="loader__ruler-track">
                    <div class="loader__ruler-fill" id="loader-fill"></div>
                    <div class="loader__ruler-ticks">
                        <span></span><span></span><span></span><span></span>
                        <span></span><span></span><span></span><span></span>
                        <span></span><span></span><span></span>
                    </div>
                </div>
                <div class="loader__ruler-meta">
                    <span class="loader__label">Preparing workspace</span>
                    <span class="loader__counter" id="loader-counter">000</span>
                </div>
            </div>
        </div>
    </div>

    <section class="showcase" id="showcase">
        <!-- ========== SCENE LAYER: wooden desk, mat, coaster, tools ========== -->
        <div class="scene" id="scene">

            <!-- Wooden desk base -->
            <div class="scene__desk" data-el="desk"></div>

            <!-- Cutting mat with grid + measurements -->
            <div class="scene__mat" data-el="mat">
                <svg class="scene__mat-grid" viewBox="0 0 1200 800" preserveAspectRatio="none">
                    <defs>
                        <pattern id="smallGrid" width="20" height="20" patternUnits="userSpaceOnUse">
                            <path d="M 20 0 L 0 0 0 20" fill="none" stroke="rgba(220, 220, 200, 0.22)" stroke-width="0.6"/>
                        </pattern>
                        <pattern id="bigGrid" width="100" height="100" patternUnits="userSpaceOnUse">
                            <rect width="100" height="100" fill="url(#smallGrid)"/>
                            <path d="M 100 0 L 0 0 0 100" fill="none" stroke="rgba(220, 220, 200, 0.38)" stroke-width="1"/>
                        </pattern>
                    </defs>
                    <rect width="100%" height="100%" fill="url(#bigGrid)"/>
                    <!-- Top ruler numbers -->
                    <g font-family="JetBrains Mono, monospace" font-size="16" fill="rgba(235, 235, 215, 0.55)" font-weight="600">
                        <text x="100" y="28">10</text>
                        <text x="200" y="28">20</text>
                        <text x="300" y="28">30</text>
                        <text x="400" y="28">40</text>
                        <text x="500" y="28">50</text>
                        <text x="600" y="28">60</text>
                        <text x="700" y="28">70</text>
                        <text x="800" y="28">80</text>
                        <text x="900" y="28">90</text>
                        <text x="1000" y="28">100</text>
                        <text x="1100" y="28">110</text>
                    </g>
                    <!-- Bottom ruler numbers -->
                    <g font-family="JetBrains Mono, monospace" font-size="16" fill="rgba(235, 235, 215, 0.55)" font-weight="600">
                        <text x="100" y="780">130</text>
                        <text x="200" y="780">140</text>
                        <text x="300" y="780">150</text>
                        <text x="400" y="780">160</text>
                        <text x="500" y="780">170</text>
                        <text x="600" y="780">180</text>
                        <text x="700" y="780">190</text>
                        <text x="800" y="780">200</text>
                        <text x="900" y="780">210</text>
                        <text x="1000" y="780">220</text>
                        <text x="1100" y="780">230</text>
                    </g>
                    <!-- Left vertical ruler numbers -->
                    <g font-family="JetBrains Mono, monospace" font-size="14" fill="rgba(235, 235, 215, 0.55)" font-weight="600">
                        <text x="8" y="120">30</text>
                        <text x="8" y="220">40</text>
                        <text x="8" y="320">50</text>
                        <text x="8" y="420">60</text>
                        <text x="8" y="520">70</text>
                        <text x="8" y="620">80</text>
                        <text x="8" y="720">90</text>
                    </g>
                </svg>
            </div>

            <!-- Cork coaster (center of mat) -->
            <div class="scene__coaster" data-el="coaster">
                <div class="scene__coaster-rim"></div>
                <div class="scene__coaster-well"></div>
            </div>

            <!-- Paperclip 1 (left edge) -->
            <div class="scene__paperclip scene__paperclip--1" data-el="paperclip-1">
                <svg viewBox="0 0 80 140">
                    <path d="M 30 15 L 30 115 Q 30 130 45 130 Q 60 130 60 115 L 60 30 Q 60 20 50 20 Q 40 20 40 30 L 40 105"
                          fill="none" stroke="#c9c3a8" stroke-width="5" stroke-linecap="round"/>
                </svg>
            </div>

            <!-- Paperclip 2 (left edge, slightly offset) -->
            <div class="scene__paperclip scene__paperclip--2" data-el="paperclip-2">
                <svg viewBox="0 0 80 140">
                    <path d="M 30 15 L 30 115 Q 30 130 45 130 Q 60 130 60 115 L 60 30 Q 60 20 50 20 Q 40 20 40 30 L 40 105"
                          fill="none" stroke="#d4cdb0" stroke-width="5" stroke-linecap="round"/>
                </svg>
            </div>

            <!-- Short pencil tip (bottom-left, poking in) -->
            <div class="scene__pencil-short" data-el="pencil-short">
                <svg viewBox="0 0 260 32">
                    <rect x="40" y="6" width="200" height="20" fill="#dcb775" rx="1"/>
                    <rect x="40" y="10" width="200" height="2" fill="#b38e4a" opacity="0.7"/>
                    <rect x="40" y="20" width="200" height="2" fill="#b38e4a" opacity="0.7"/>
                    <polygon points="40,6 40,26 12,16" fill="#e9d6a8"/>
                    <polygon points="12,16 2,14 2,18" fill="#1a1a1a"/>
                    <rect x="240" y="6" width="18" height="20" fill="#3a3a3a"/>
                </svg>
            </div>

            <!-- Long pencil (top-right, angled) -->
            <div class="scene__pencil-long" data-el="pencil-long">
                <svg viewBox="0 0 520 34">
                    <rect x="50" y="5" width="420" height="24" fill="#efe2c4" rx="1"/>
                    <rect x="50" y="10" width="420" height="1.5" fill="#c9b584" opacity="0.6"/>
                    <rect x="50" y="23" width="420" height="1.5" fill="#c9b584" opacity="0.6"/>
                    <polygon points="50,5 50,29 16,17" fill="#f5ead0"/>
                    <polygon points="16,17 4,15 4,19" fill="#1a1a1a"/>
                    <rect x="470" y="5" width="30" height="24" fill="#8a8679"/>
                    <rect x="500" y="5" width="16" height="24" fill="#d46a5a" rx="1"/>
                </svg>
            </div>

            <!-- Box cutter (bottom-right) -->
            <div class="scene__cutter" data-el="cutter">
                <svg viewBox="0 0 320 72">
                    <!-- Blade -->
                    <polygon points="260,20 318,26 318,44 260,50" fill="#d8d4c6"/>
                    <polygon points="260,20 300,24 300,46 260,50" fill="#b8b3a0" opacity="0.7"/>
                    <line x1="270" y1="28" x2="300" y2="32" stroke="#8a8678" stroke-width="0.6"/>
                    <line x1="270" y1="42" x2="300" y2="40" stroke="#8a8678" stroke-width="0.6"/>
                    <!-- Body -->
                    <rect x="6" y="12" width="260" height="48" fill="#e89a3c" rx="6"/>
                    <rect x="6" y="12" width="260" height="14" fill="#f2ae54" rx="6"/>
                    <!-- Slider groove -->
                    <rect x="30" y="26" width="140" height="18" fill="#a85f1d" rx="3"/>
                    <!-- Slider thumb -->
                    <rect x="70" y="22" width="36" height="28" fill="#2a2a2a" rx="3"/>
                    <rect x="75" y="28" width="3" height="16" fill="#555"/>
                    <rect x="85" y="28" width="3" height="16" fill="#555"/>
                    <rect x="95" y="28" width="3" height="16" fill="#555"/>
                    <!-- End cap -->
                    <rect x="6" y="12" width="14" height="48" fill="#c97e26" rx="6"/>
                </svg>
            </div>

            <!-- Vignette overlay -->
            <div class="scene__vignette"></div>
        </div>

        <!-- ========== UI LAYER ========== -->

        <!-- Top-left pagination dot -->
        <div class="showcase__pager" data-fade>
            <span class="showcase__pager-dot"></span>
        </div>

        <!-- Top tagline -->
        <div class="showcase__tagline" data-fade>
            CRAFTED FOR EVERYDAY MOMENTS.
        </div>

        <!-- Navigation -->
        <nav class="showcase__nav" aria-label="Section navigation">
            <ul>
                <li data-nav><a href="#intro" class="is-active">INTRO</a></li>
                <li data-nav><a href="#features">FEATURES</a></li>
                <li data-nav><a href="#product">PRODUCT</a></li>
                <li data-nav><a href="#contact">CONTACT</a></li>
            </ul>
        </nav>

        <!-- Right-edge vertical sticker -->
        <aside class="showcase__sticker" data-sticker>
            <span>MODEL 01 ยท EDITION A</span>
        </aside>

        <!-- Huge product name -->
        <h1 class="showcase__title" data-title>
            <span class="showcase__title-mask"><span class="showcase__title-letter">B</span></span>
            <span class="showcase__title-mask"><span class="showcase__title-letter">R</span></span>
            <span class="showcase__title-mask"><span class="showcase__title-letter">A</span></span>
            <span class="showcase__title-mask"><span class="showcase__title-letter">N</span></span>
            <span class="showcase__title-mask"><span class="showcase__title-letter">D</span></span>
        </h1>

        <!-- Right-side description -->
        <p class="showcase__description" data-words>
            Engineered with precision and purpose. Designed to elevate the simplest interactions into considered experiences.
        </p>

        <!-- Bottom-left frosted glass card -->
        <div class="showcase__card" data-card>
            <h2 class="showcase__card-title">
                CREATED<br>
                BY STUDIO NAME,<br>
                AN AWARD-WINNING<br>
                DESIGN PRACTICE.
            </h2>
            <div class="showcase__card-divider"></div>
            <p class="showcase__card-sub">
                A thoughtfully designed object<br>for the modern workspace.
            </p>
        </div>

        <!-- Scroll indicator -->
        <div class="showcase__scroll" data-fade>
            <div class="showcase__scroll-icon">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                    <path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
            </div>
            <span>SCROLL TO CONTINUE</span>
        </div>

        <!-- Bottom-right video thumbnail -->
        <a href="#" class="showcase__video" data-video>
            <div class="showcase__video-thumb">
                <img src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=400&q=80"
                     alt="Watch film"
                     draggable="false"
                     onerror="this.style.display='none'">
            </div>
            <div class="showcase__video-label">
                <span class="showcase__video-title">BRAND</span>
                <span class="showcase__video-play">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
                    PLAY
                </span>
            </div>
        </a>
    </section>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
    <script src="banner-product-showcase.js"></script>
</body>
</html>

</body>
</html>
JSbanner-product-showcase.jsโ€” GSAP animation timeline
(() => {
    const showcase = document.getElementById('showcase');
    if (!showcase) return;

    // -------------------------------------------------------------------
    //  SPLIT DESCRIPTION INTO WORD SPANS (for masked word reveal)
    // -------------------------------------------------------------------
    const desc = showcase.querySelector('[data-words]');
    if (desc) {
        const words = desc.textContent.trim().split(/\s+/);
        desc.textContent = '';
        words.forEach((w, i) => {
            const mask = document.createElement('span');
            mask.className = 'word-mask';
            const word = document.createElement('span');
            word.className = 'word';
            word.textContent = w;
            mask.appendChild(word);
            desc.appendChild(mask);
            if (i < words.length - 1) desc.appendChild(document.createTextNode(' '));
        });
    }

    // -------------------------------------------------------------------
    //  CACHE ELEMENT REFERENCES
    // -------------------------------------------------------------------
    const loader = document.getElementById('loader');
    const loaderFill = document.getElementById('loader-fill');
    const loaderCounter = document.getElementById('loader-counter');
    const loaderPanelTop = loader.querySelector('.loader__panel--top');
    const loaderPanelBottom = loader.querySelector('.loader__panel--bottom');
    const loaderStroke = loader.querySelector('.loader__text-stroke');
    const loaderFillText = loader.querySelector('.loader__text-fill');
    const loaderTicks = loader.querySelectorAll('.loader__ruler-ticks span');
    const loaderMeta = loader.querySelector('.loader__ruler-meta');
    const loaderBrand = loader.querySelector('.loader__brand');
    const loaderRuler = loader.querySelector('.loader__ruler');

    const desk = showcase.querySelector('[data-el="desk"]');
    const mat = showcase.querySelector('[data-el="mat"]');
    const coaster = showcase.querySelector('[data-el="coaster"]');
    const pencilLong = showcase.querySelector('[data-el="pencil-long"]');
    const pencilShort = showcase.querySelector('[data-el="pencil-short"]');
    const paperclip1 = showcase.querySelector('[data-el="paperclip-1"]');
    const paperclip2 = showcase.querySelector('[data-el="paperclip-2"]');
    const cutter = showcase.querySelector('[data-el="cutter"]');

    const titleLetters = showcase.querySelectorAll('.showcase__title-letter');
    const words = showcase.querySelectorAll('.word');
    const fades = showcase.querySelectorAll('[data-fade]');
    const navItems = showcase.querySelectorAll('[data-nav]');
    const sticker = showcase.querySelector('[data-sticker]');
    const card = showcase.querySelector('[data-card]');
    const cardDivider = showcase.querySelector('.showcase__card-divider');
    const video = showcase.querySelector('[data-video]');
    const tagline = showcase.querySelector('.showcase__tagline');

    // -------------------------------------------------------------------
    //  CAPTURE SCENE TRANSFORMS (CSS already rotates/positions elements โ€”
    //  preserve those, animate around them)
    // -------------------------------------------------------------------
    const sceneEls = [
        { el: mat, rotation: -1.5 },
        { el: pencilLong, rotation: -68 },
        { el: pencilShort, rotation: -18 },
        { el: paperclip1, rotation: -25 },
        { el: paperclip2, rotation: 15 },
        { el: cutter, rotation: 18 },
    ];

    // -------------------------------------------------------------------
    //  INITIAL HIDDEN STATES (before loader finishes)
    // -------------------------------------------------------------------
    gsap.set([tagline], { y: -20, opacity: 0 });
    gsap.set(navItems, { y: -20, opacity: 0 });
    gsap.set(sticker, { x: 120, opacity: 0 });
    gsap.set(card, { x: -60, opacity: 0, scale: 0.96 });
    gsap.set(cardDivider, { scaleX: 0 });
    gsap.set(video, { y: 80, opacity: 0, scale: 0.85 });
    gsap.set(fades, { y: 20 });

    gsap.set(desk, { opacity: 0, scale: 1.05 });
    gsap.set(mat, { opacity: 0, scale: 0.88, rotation: -1.5, y: 40 });
    gsap.set(coaster, { opacity: 0, scale: 0.2, rotation: -180 });
    gsap.set(pencilLong, { opacity: 0, x: 400, y: -400, rotation: -68 });
    gsap.set(pencilShort, { opacity: 0, x: -300, rotation: -18 });
    gsap.set(paperclip1, { opacity: 0, x: -150, rotation: -25 });
    gsap.set(paperclip2, { opacity: 0, x: -150, rotation: 15 });
    gsap.set(cutter, { opacity: 0, x: 400, y: 200, rotation: 18 });

    // -------------------------------------------------------------------
    //  LOADER TIMELINE
    // -------------------------------------------------------------------
    const loaderTl = gsap.timeline({
        onComplete: playScene,
    });

    // 1. Stroke-draw the ORYZO wordmark
    loaderTl.to(loaderStroke, {
        strokeDashoffset: 0,
        duration: 1.4,
        ease: 'power2.inOut',
    }, 0);

    // 2. Tick marks stagger in
    loaderTl.from(loaderTicks, {
        scaleY: 0,
        opacity: 0,
        duration: 0.5,
        stagger: 0.03,
        ease: 'power2.out',
        transformOrigin: 'bottom',
    }, 0.1);

    // 3. Counter + fill bar progress 0 -> 100
    const progress = { v: 0 };
    loaderTl.to(progress, {
        v: 100,
        duration: 1.8,
        ease: 'power1.inOut',
        onUpdate: () => {
            const val = Math.round(progress.v);
            loaderFill.style.width = val + '%';
            loaderCounter.textContent = String(val).padStart(3, '0');
        },
    }, 0.15);

    // 4. Fill the stroked text with solid color
    loaderTl.to(loaderFillText, {
        opacity: 1,
        duration: 0.5,
        ease: 'power2.out',
    }, 1.4);

    // 5. Brief beat
    loaderTl.to({}, { duration: 0.25 });

    // 6. Pre-exit: brand + ruler shrink/fade
    loaderTl.to([loaderBrand, loaderRuler], {
        y: -20,
        opacity: 0,
        duration: 0.5,
        ease: 'power3.in',
    }, '+=0.1');

    // 7. Panels split apart
    loaderTl.to(loaderPanelTop, {
        yPercent: -100,
        duration: 1.1,
        ease: 'power4.inOut',
    }, '-=0.1');
    loaderTl.to(loaderPanelBottom, {
        yPercent: 100,
        duration: 1.1,
        ease: 'power4.inOut',
    }, '<');

    // 8. Hide loader once panels clear
    loaderTl.set(loader, { display: 'none' });

    // -------------------------------------------------------------------
    //  SCENE + UI ENTRY (triggered after loader completes)
    // -------------------------------------------------------------------
    function playScene() {
        const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

        // Desk fades in as the base
        tl.to(desk, {
            opacity: 1,
            scale: 1,
            duration: 1.4,
            ease: 'power2.out',
        }, 0);

        // Cutting mat drops down with subtle rotate settle
        tl.to(mat, {
            opacity: 1,
            scale: 1,
            y: 0,
            duration: 1.2,
            ease: 'power4.out',
        }, 0.15);

        // Coaster: spins in with settle (elastic)
        tl.to(coaster, {
            opacity: 1,
            scale: 1,
            rotation: 0,
            duration: 1.6,
            ease: 'elastic.out(1, 0.65)',
        }, 0.55);

        // Long pencil: swoops in from top-right on an arc, rotates into rest
        tl.to(pencilLong, {
            opacity: 1,
            x: 0,
            y: 0,
            duration: 1.1,
            ease: 'power4.out',
        }, 0.75);

        // Short pencil: slides in from left
        tl.to(pencilShort, {
            opacity: 1,
            x: 0,
            duration: 1,
            ease: 'power4.out',
        }, 0.9);

        // Paperclips: slide in from left with small stagger
        tl.to(paperclip1, {
            opacity: 1,
            x: 0,
            duration: 0.9,
            ease: 'power3.out',
        }, 1);
        tl.to(paperclip2, {
            opacity: 1,
            x: 0,
            duration: 0.9,
            ease: 'power3.out',
        }, 1.08);

        // Cutter: slides in from bottom-right
        tl.to(cutter, {
            opacity: 1,
            x: 0,
            y: 0,
            duration: 1,
            ease: 'power4.out',
        }, 1.1);

        // Small bounce/settle on each after arrival to feel physical
        tl.to(coaster, { scale: 1.02, duration: 0.2, yoyo: true, repeat: 1, ease: 'sine.inOut' }, 2);

        // ---------- UI layer ----------
        tl.to(tagline, {
            y: 0, opacity: 1, duration: 0.7,
        }, 0.4);

        tl.to(navItems, {
            y: 0, opacity: 1, duration: 0.7, stagger: 0.08,
        }, 0.5);

        tl.to(sticker, {
            x: 0, opacity: 1, duration: 0.9, ease: 'power4.out',
        }, 0.6);

        tl.to(titleLetters, {
            y: '0%', duration: 1.1, stagger: 0.07, ease: 'expo.out',
        }, 0.85);

        tl.to(words, {
            y: '0%', duration: 0.8, stagger: 0.025, ease: 'power3.out',
        }, 1.3);

        tl.to(card, {
            x: 0, opacity: 1, scale: 1, duration: 1, ease: 'power4.out',
        }, 1.45);

        tl.to(cardDivider, {
            scaleX: 1, duration: 0.8, ease: 'power2.inOut',
        }, 1.85);

        tl.to(fades, {
            y: 0, opacity: 1, duration: 0.7, stagger: 0.15,
        }, 1.7);

        tl.to(video, {
            y: 0, opacity: 1, scale: 1, duration: 0.9, ease: 'back.out(1.6)',
        }, 2.05);

        // Kick off ticker-based parallax once everything has settled
        tl.call(startParallax, null, 2.2);
    }

    // -------------------------------------------------------------------
    //  MOUSE PARALLAX (each layer moves at a different depth)
    // -------------------------------------------------------------------
    let targetX = 0, targetY = 0;
    let currentX = 0, currentY = 0;
    let parallaxActive = false;

    function startParallax() {
        parallaxActive = true;
    }

    const onMouseMove = (e) => {
        const rect = showcase.getBoundingClientRect();
        targetX = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
        targetY = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
    };
    const onMouseLeave = () => { targetX = 0; targetY = 0; };

    showcase.addEventListener('mousemove', onMouseMove);
    showcase.addEventListener('mouseleave', onMouseLeave);

    const title = showcase.querySelector('.showcase__title');
    const descEl = showcase.querySelector('.showcase__description');

    gsap.ticker.add(() => {
        if (!parallaxActive) return;

        currentX += (targetX - currentX) * 0.06;
        currentY += (targetY - currentY) * 0.06;

        // Deepest (desk) moves least; nearest (tools) move more.
        gsap.set(desk, { x: -currentX * 12, y: -currentY * 8 });
        gsap.set(mat, { x: -currentX * 18, y: -currentY * 12, rotation: -1.5 + currentX * 0.6 });
        gsap.set(coaster, { x: currentX * 8, y: currentY * 6 });

        gsap.set(pencilLong, { x: currentX * 24, y: currentY * 16, rotation: -68 + currentX * 1.2 });
        gsap.set(pencilShort, { x: currentX * 20, y: currentY * 14, rotation: -18 - currentX * 0.8 });
        gsap.set(paperclip1, { x: currentX * 22, y: currentY * 18, rotation: -25 + currentY * 0.6 });
        gsap.set(paperclip2, { x: currentX * 26, y: currentY * 22, rotation: 15 - currentY * 0.5 });
        gsap.set(cutter, { x: currentX * 28, y: currentY * 20, rotation: 18 + currentX * 1.4 });

        // UI layer โ€” drifts subtly with mouse
        gsap.set(title, { x: currentX * 14, y: currentY * 8 });
        gsap.set(descEl, { x: currentX * 8, y: currentY * 5 });
        gsap.set(card, { x: currentX * 6, y: currentY * 3 });
    });

    // -------------------------------------------------------------------
    //  NAV ACTIVE STATE
    // -------------------------------------------------------------------
    const navLinks = showcase.querySelectorAll('.showcase__nav a');
    navLinks.forEach((link) => {
        link.addEventListener('click', (e) => {
            e.preventDefault();
            navLinks.forEach(l => l.classList.remove('is-active'));
            link.classList.add('is-active');
        });
    });
})();

More GSAP landing pages