ToolsWaves
Free GSAP Landing Page Template · ToolsWaves · Portfolio

Free Designer Portfolio Hero Template with Vinyl Loader

Free dark designer portfolio landing page template from ToolsWaves with a vinyl-ring loader, rotating background disc, portrait parallax, magnetic nav, character-proximity title, dot ripple, play ring, and scrolling service marquee. Free for any use.

PortfolioVinylMarqueeMagnetic NavCinematic

About this ToolsWaves template

This free designer portfolio landing page template from ToolsWaves uses a distinct music-industry visual treatment — a vinyl-ring loader, a rotating background disc that continues to spin after load, and a scrolling marquee of services along the bottom. The hero features a parallax portrait that shifts with mouse movement, magnetic navigation that pulls toward the cursor, and title characters that react individually to proximity. A dot ripple and play-button ring add ambient motion without distracting from the focus on the designer's identity.

Use this template for individual designers, music-adjacent creatives, illustrators, or any portfolio where the maker's personal brand is the product being sold. ToolsWaves provides this template completely free with no licensing fees — copy the three files into your project, swap the placeholder portrait for your own photo, replace the service marquee with your actual offerings, and update the brand identity throughout. The vinyl and disc imagery can be customized via CSS or replaced entirely if you want a different aesthetic anchor.

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 Designer Portfolio Hero Template with Vinyl Loader</title>
  <meta name="description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
  <meta name="generator" content="ToolsWaves - https://toolswaves.in/landing-pages" />

  <!-- Open Graph -->
  <meta property="og:title" content="Free Designer Portfolio Hero Template with Vinyl Loader" />
  <meta property="og:description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
  <meta property="og:type" content="website" />
  <meta property="og:image" content="https://toolswaves.in/og?title=Free%20Designer%20Portfolio%20Hero%20Template%20with%20Vinyl%20Loader&category=Landing%20Page&icon=%F0%9F%93%84" />

  <!-- Twitter -->
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="Free Designer Portfolio Hero Template with Vinyl Loader" />
  <meta name="twitter:description" content="Free GSAP designer portfolio landing page hero from ToolsWaves" />
  <meta name="twitter:image" content="https://toolswaves.in/og?title=Free%20Designer%20Portfolio%20Hero%20Template%20with%20Vinyl%20Loader&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 {
    --bg:     #0a0a0a;
    --ink:    #ffffff;
    --muted:  rgba(255, 255, 255, 0.55);
    --line:   rgba(255, 255, 255, 0.12);
    --lime:   #c9ff2d;
    --lime-deep: #a8dd15;
    --accent: #c9ff2d;
}

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

.back-link {
    position: fixed;
    bottom: 4.5rem;
    left: 0.75rem;
    z-index: 1000;
    color: var(--ink);
    text-decoration: none;
    font-size: 0.65rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    padding: 0.45rem 0.9rem;
    background: rgba(255, 255, 255, 0.08);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 999px;
    backdrop-filter: blur(8px);
    opacity: 0.85;
    transition: opacity 0.3s, transform 0.3s, background 0.3s;
}
.back-link:hover { opacity: 1; transform: translateY(-2px); background: rgba(255, 255, 255, 0.14); }

/* =========================================================
   LOADER — rotating vinyl with counter
   ========================================================= */
.loader {
    position: fixed;
    inset: 0;
    z-index: 999;
    background: var(--bg);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.5rem;
}

.loader__stage {
    position: relative;
    width: min(320px, 55vw);
    height: min(320px, 55vw);
    display: flex;
    align-items: center;
    justify-content: center;
}

.loader__vinyl {
    width: 100%;
    height: 100%;
    filter: drop-shadow(0 20px 60px rgba(201, 255, 45, 0.15));
}

.loader__counter {
    position: absolute;
    font-family: 'JetBrains Mono', monospace;
    font-size: 1rem;
    font-weight: 600;
    color: var(--bg);
    letter-spacing: 0.2em;
    font-variant-numeric: tabular-nums;
    z-index: 2;
}

.loader__label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.75rem;
    letter-spacing: 0.3em;
    color: var(--muted);
    text-transform: uppercase;
}

/* =========================================================
   PORTFOLIO SECTION
   ========================================================= */
.portfolio {
    position: relative;
    width: 100%;
    height: 100vh;
    min-height: 780px;
    overflow: hidden;
    background: var(--bg);
}

/* ---------- Top nav ---------- */
.topnav {
    position: relative;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 2rem;
    padding: 1rem 1.5rem;
    border-bottom: 1px solid var(--line);
    background: rgba(10, 10, 10, 0.65);
    backdrop-filter: blur(8px);
}

.topnav__logo {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--ink);
    text-decoration: none;
    font-weight: 700;
    font-size: 1.35rem;
    letter-spacing: -0.01em;
    will-change: transform;
}
.topnav__logo-mark { width: 36px; height: 36px; flex-shrink: 0; }
.topnav__logo-mark svg { width: 100%; height: 100%; display: block; }

.topnav__links {
    margin: 0 auto;
    display: flex;
    gap: 2.5rem;
}
.topnav__links a {
    position: relative;
    color: var(--ink);
    text-decoration: none;
    font-weight: 600;
    font-size: 0.85rem;
    letter-spacing: 0.1em;
    padding: 0.3rem 0;
    will-change: transform;
}
.topnav__links a.is-active { color: var(--lime); }
.topnav__links a::after {
    content: '';
    position: absolute;
    left: 0; bottom: -2px;
    width: 100%; height: 1px;
    background: var(--lime);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.topnav__links a.is-active::after { transform: scaleX(1); transform-origin: left; }
.topnav__links a:hover::after { transform: scaleX(1); transform-origin: left; }

.topnav__actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.topnav__cta {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: var(--lime);
    color: var(--bg);
    text-decoration: none;
    padding: 0.75rem 1.2rem;
    border-radius: 999px;
    font-weight: 600;
    font-size: 0.9rem;
    will-change: transform;
    transition: background 0.3s;
}
.topnav__cta svg { width: 16px; height: 16px; transition: transform 0.3s ease; }
.topnav__cta:hover { background: var(--lime-deep); }
.topnav__cta:hover svg { transform: translateX(3px); }

.topnav__menu {
    width: 44px; height: 44px;
    background: var(--ink);
    border: none;
    border-radius: 8px;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    padding: 0;
    will-change: transform;
}
.topnav__menu span {
    width: 18px;
    height: 2px;
    background: var(--bg);
    border-radius: 2px;
    transition: all 0.3s ease;
}
.topnav__menu:hover span:nth-child(1) { transform: translateY(1px); }
.topnav__menu:hover span:nth-child(3) { transform: translateY(-1px); }

/* ---------- Left rail ---------- */
.rail {
    position: absolute;
    z-index: 15;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
}
.rail > * { pointer-events: auto; }
.rail--left {
    top: 7rem; left: 0.5rem; bottom: 5rem;
    gap: 2rem;
}
.rail--right {
    top: 7rem; right: 0.75rem; bottom: 5rem;
    gap: 1.5rem;
    align-items: center;
}

.rail__dots {
    display: grid;
    grid-template-columns: repeat(3, 6px);
    gap: 4px;
}
.rail__dots span {
    width: 6px; height: 6px;
    border-radius: 50%;
    background: var(--ink);
    cursor: pointer;
    will-change: transform;
    transition: background 0.3s ease;
}
.rail__dots span:hover { background: var(--lime); }

.rail__phone {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    color: var(--ink);
    text-decoration: none;
    font-size: 0.75rem;
    letter-spacing: 0.25em;
    font-family: 'JetBrains Mono', monospace;
    transition: color 0.3s ease;
}
.rail__phone:hover { color: var(--lime); }

.rail__scroll {
    margin-top: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.65rem;
    letter-spacing: 0.35em;
    color: var(--muted);
}
.rail__scroll-label {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
}
.rail__scroll-line {
    width: 1px;
    height: 80px;
    background: linear-gradient(180deg, rgba(255,255,255,0.4), transparent);
    display: block;
    transform-origin: top;
}
.rail__scroll-arrow { font-size: 0.8rem; opacity: 0.6; }

.rail__follow {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.65rem;
    letter-spacing: 0.35em;
    color: var(--muted);
}
.rail__follow-label {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
}
.rail__follow-line {
    width: 1px;
    height: 70px;
    background: linear-gradient(180deg, rgba(255,255,255,0.4), transparent);
    display: block;
}
.rail__follow-arrow { font-size: 0.8rem; opacity: 0.6; }

.rail__social {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    margin-top: auto;
    align-items: center;
}
.rail__social-icon {
    width: 34px; height: 34px;
    border-radius: 50%;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.12);
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    font-size: 0.75rem;
    font-weight: 700;
    will-change: transform;
    transition: background 0.3s ease, color 0.3s ease;
}
.rail__social-icon svg { width: 15px; height: 15px; }
.rail__social-icon:hover { background: var(--lime); color: var(--bg); }

/* ---------- Stage (vinyl + portrait) ---------- */
.stage {
    position: absolute;
    inset: 0;
    z-index: 5;
    pointer-events: none;
    overflow: hidden;
}

.stage__vinyl {
    position: absolute;
    top: 45%;
    left: 58%;
    transform: translate(-50%, -50%);
    width: min(820px, 85vw);
    height: min(820px, 85vw);
    opacity: 0.95;
    will-change: transform;
}

.stage__portrait {
    position: absolute;
    top: 46%;
    left: 66%;
    transform: translate(-50%, -50%);
    width: min(480px, 45vw);
    height: min(620px, 80vh);
    will-change: transform;
    mask-image: radial-gradient(ellipse at center, #000 55%, transparent 95%);
    -webkit-mask-image: radial-gradient(ellipse at center, #000 55%, transparent 95%);
}
.stage__portrait img {
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: center top;
    user-select: none;
    filter: contrast(1.05) saturate(0.9);
}

/* ---------- Main hero content ---------- */
.hero {
    position: absolute;
    left: 5rem;
    top: 7rem;
    right: 4rem;
    bottom: 5rem;
    z-index: 8;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    gap: 1.5rem;
    pointer-events: none;
}
.hero > * { pointer-events: auto; }

.hero__eyebrow {
    position: relative;
    display: inline-flex;
    align-items: flex-start;
    gap: 0.4rem;
    color: var(--ink);
    text-decoration: none;
    font-size: 0.95rem;
    font-weight: 500;
    line-height: 1.4;
    padding-bottom: 0.4rem;
    max-width: max-content;
    will-change: transform;
}
.hero__eyebrow-text {
    border-bottom: 1px solid rgba(255, 255, 255, 0.4);
    padding-bottom: 0.2rem;
}
.hero__eyebrow-arrow {
    display: inline-block;
    font-size: 0.85em;
    transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.hero__eyebrow:hover .hero__eyebrow-arrow {
    transform: translate(4px, -4px) rotate(3deg);
}

.hero__title {
    font-family: 'Inter', sans-serif;
    font-weight: 800;
    font-size: clamp(3rem, 9vw, 8rem);
    line-height: 0.95;
    letter-spacing: -0.04em;
    color: var(--ink);
    margin: auto 0;
    max-width: 780px;
}
.hero__line {
    display: block;
    overflow: hidden;
    padding-bottom: 0.05em;
}
.hero__char {
    display: inline-block;
    will-change: transform, color;
    transition: color 0.3s ease;
}
.hero__char.is-space { width: 0.25em; }

.hero__play {
    display: inline-flex;
    align-items: center;
    gap: 1rem;
    text-decoration: none;
    color: var(--ink);
    margin-top: auto;
    max-width: max-content;
    will-change: transform;
}
.hero__play-scribble { width: 70px; height: 52px; }
.hero__play-scribble path {
    stroke-dasharray: 120;
    stroke-dashoffset: 120;
}
.hero__play-btn {
    position: relative;
    width: 62px;
    height: 62px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.hero__play-btn > svg:first-child {
    width: 100%; height: 100%;
    transition: transform 0.3s ease;
}
.hero__play-ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    will-change: transform;
}
.hero__play:hover .hero__play-btn > svg:first-child {
    transform: scale(1.08);
}
.hero__play-label {
    display: flex;
    flex-direction: column;
    font-weight: 500;
    font-size: 1rem;
    line-height: 1.1;
}

/* ---------- Marquee ---------- */
.marquee {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    z-index: 10;
    background: var(--lime);
    color: var(--bg);
    padding: 1.1rem 0;
    overflow: hidden;
    white-space: nowrap;
}
.marquee__track {
    display: flex;
    gap: 0;
    width: max-content;
    will-change: transform;
}
.marquee__group {
    display: flex;
    gap: 3rem;
    padding: 0 1.5rem;
    align-items: center;
}
.marquee__item {
    font-size: clamp(1rem, 1.7vw, 1.5rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    white-space: nowrap;
    cursor: default;
}

/* ---------- Initial hidden states ---------- */
[data-magnetic], [data-rail], [data-social], [data-eyebrow],
[data-title-line] .hero__char, [data-play], [data-portrait] {
    opacity: 0;
}

/* ---------- Responsive ---------- */
@media (max-width: 1100px) {
    .topnav__links { display: none; }
    .hero { left: 4rem; right: 3rem; top: 6rem; }
    .rail--left { left: 0.25rem; }
    .rail--right { right: 0.25rem; }
    .stage__portrait { left: 72%; width: 55vw; }
    .stage__vinyl { left: 62%; width: 90vw; height: 90vw; }
}
@media (max-width: 768px) {
    .rail--right .rail__follow { display: none; }
    .rail--left .rail__phone { display: none; }
    .rail__social { gap: 0.4rem; }
    .rail__social-icon { width: 28px; height: 28px; }
    .hero { left: 3rem; right: 3rem; top: 5rem; bottom: 4rem; }
    .hero__title { font-size: clamp(2.3rem, 11vw, 6rem); }
    .stage__portrait { left: 55%; width: 75vw; top: 50%; opacity: 0.35; }
    .stage__vinyl { left: 50%; width: 110vw; height: 110vw; opacity: 0.7; }
    .topnav__menu { width: 38px; height: 38px; }
}
@media (max-width: 480px) {
    .rail { gap: 1rem; }
    .rail--left { top: 5rem; }
    .rail--right { top: 5rem; }
    .rail__scroll, .rail__follow { display: none; }
    .rail__dots { display: none; }
    .topnav { padding: 0.75rem 1rem; gap: 0.75rem; }
    .topnav__logo-text { display: none; }
    .topnav__cta { padding: 0.5rem 0.9rem; font-size: 0.8rem; }
    .hero__eyebrow { font-size: 0.85rem; }
}

  </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 — Portfolio Dark</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=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="banner-portfolio-dark.css">
</head>
<body>
    

    <!-- ========== LOADER ========== -->
    <div class="loader" id="loader">
        <div class="loader__stage">
            <svg class="loader__vinyl" id="loader-vinyl" viewBox="0 0 400 400" aria-hidden="true">
                <g id="loader-rings"></g>
                <circle cx="200" cy="200" r="62" fill="#c9ff2d"/>
                <circle cx="200" cy="200" r="62" fill="none" stroke="#0a0a0a" stroke-width="1" stroke-dasharray="2 4"/>
                <circle cx="200" cy="200" r="7" fill="#0a0a0a"/>
            </svg>
            <div class="loader__counter" id="loader-counter">000</div>
        </div>
        <div class="loader__label" id="loader-label">LOADING SESSION</div>
    </div>

    <section class="portfolio" id="portfolio">

        <!-- ========== TOP NAV ========== -->
        <header class="topnav">
            <a href="#" class="topnav__logo" data-magnetic>
                <span class="topnav__logo-mark">
                    <svg viewBox="0 0 40 40">
                        <circle cx="20" cy="20" r="20" fill="#c9ff2d"/>
                        <path d="M20 8v24M8 20h24M12 12l16 16M28 12L12 28" stroke="#0a0a0a" stroke-width="1.5"/>
                    </svg>
                </span>
                <span class="topnav__logo-text">Brand</span>
            </a>
            <nav class="topnav__links">
                <a href="#" class="is-active" data-magnetic data-link>Home</a>
                <a href="#" data-magnetic data-link>About</a>
                <a href="#" data-magnetic data-link>Works</a>
                <a href="#" data-magnetic data-link>Services</a>
                <a href="#" data-magnetic data-link>Testimonial</a>
                <a href="#" data-magnetic data-link>Blog</a>
                <a href="#" data-magnetic data-link>Contact</a>
            </nav>
            <div class="topnav__actions">
                <a href="#" class="topnav__cta" data-magnetic>
                    <span>Let's Talk</span>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
                        <path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </a>
                <button class="topnav__menu" data-magnetic aria-label="Menu">
                    <span></span><span></span><span></span>
                </button>
            </div>
        </header>

        <!-- ========== LEFT RAIL ========== -->
        <aside class="rail rail--left">
            <div class="rail__dots" data-rail>
                <span></span><span></span><span></span>
                <span></span><span></span><span></span>
                <span></span><span></span><span></span>
            </div>
            <a href="#" class="rail__phone" data-rail>+(02)-574-328-301</a>
            <div class="rail__scroll" data-rail>
                <span class="rail__scroll-label">SCROLL DOWN</span>
                <span class="rail__scroll-line"></span>
                <span class="rail__scroll-arrow">&#8595;</span>
            </div>
        </aside>

        <!-- ========== RIGHT RAIL ========== -->
        <aside class="rail rail--right">
            <div class="rail__follow" data-rail>
                <span class="rail__follow-label">FOLLOW ME</span>
                <span class="rail__follow-line"></span>
                <span class="rail__follow-arrow">&#8595;</span>
            </div>
            <div class="rail__social" data-rail>
                <a href="#" class="rail__social-icon" data-social aria-label="Facebook">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 21v-8h2.7l.4-3h-3.1V8c0-.9.3-1.5 1.5-1.5h1.6V3.8c-.3 0-1.2-.1-2.3-.1-2.3 0-3.9 1.4-3.9 4V10H7.8v3h2.5v8h3.2z"/></svg>
                </a>
                <a href="#" class="rail__social-icon" data-social aria-label="Twitter">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.9 7.3c0 .2 0 .4 0 .5 0 5.4-4.1 11.6-11.6 11.6C5 19.5 2.8 18.8 1 17.6c.3 0 .6.1 1 .1 1.9 0 3.7-.7 5.1-1.8-1.8 0-3.3-1.2-3.9-2.9.3 0 .5.1.8.1.4 0 .7 0 1.1-.1-1.9-.4-3.4-2.1-3.4-4.2v-.1c.6.3 1.2.5 1.9.5C2.4 8.4 1.6 7 1.6 5.5c0-.8.2-1.5.6-2.1 2.1 2.5 5.1 4.1 8.6 4.3-.1-.3-.1-.6-.1-.9 0-2.3 1.8-4.1 4.1-4.1 1.2 0 2.3.5 3 1.3.9-.2 1.8-.5 2.6-1-.3 1-1 1.8-1.8 2.3.8-.1 1.6-.3 2.4-.6-.6.8-1.3 1.5-2.1 2.1z"/></svg>
                </a>
                <a href="#" class="rail__social-icon" data-social aria-label="LinkedIn">
                    <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 3h-15C3.7 3 3 3.7 3 4.5v15c0 .8.7 1.5 1.5 1.5h15c.8 0 1.5-.7 1.5-1.5v-15c0-.8-.7-1.5-1.5-1.5zM8.3 18H5.7V9.8h2.6V18zM7 8.6c-.8 0-1.5-.7-1.5-1.5S6.2 5.6 7 5.6s1.5.7 1.5 1.5S7.8 8.6 7 8.6zM18.3 18h-2.6v-4.2c0-1 0-2.3-1.4-2.3s-1.6 1.1-1.6 2.2V18H10.1V9.8h2.5v1.1h.1c.3-.7 1.2-1.4 2.5-1.4 2.7 0 3.2 1.8 3.2 4V18z"/></svg>
                </a>
                <a href="#" class="rail__social-icon" data-social aria-label="Behance">
                    <span>Be</span>
                </a>
                <a href="#" class="rail__social-icon" data-social aria-label="Dribbble">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                        <circle cx="12" cy="12" r="9"/>
                        <path d="M3 12c6 0 13 1 18 7M5 6c4 2 11 8 14 15M20 6c-4 4-12 6-17 4"/>
                    </svg>
                </a>
            </div>
        </aside>

        <!-- ========== CENTER VINYL + PORTRAIT ========== -->
        <div class="stage" id="stage">
            <svg class="stage__vinyl" id="stage-vinyl" viewBox="0 0 800 800" aria-hidden="true">
                <defs>
                    <radialGradient id="vinylGrad" cx="50%" cy="50%" r="50%">
                        <stop offset="0%" stop-color="#202022"/>
                        <stop offset="100%" stop-color="#0a0a0a"/>
                    </radialGradient>
                </defs>
                <circle cx="400" cy="400" r="380" fill="url(#vinylGrad)"/>
                <g id="stage-rings" fill="none"></g>
                <circle cx="400" cy="400" r="40" fill="#0a0a0a" stroke="#2a2a2a" stroke-width="1"/>
            </svg>

            <div class="stage__portrait" id="portrait" data-portrait>
                <img src="https://images.unsplash.com/photo-1568602471122-7832951cc4c5?auto=format&fit=crop&w=900&q=80"
                     alt="Portrait"
                     draggable="false"
                     onerror="this.style.display='none'">
            </div>
        </div>

        <!-- ========== MAIN CONTENT ========== -->
        <div class="hero" id="hero">
            <a href="#" class="hero__eyebrow" data-magnetic data-eyebrow>
                <span class="hero__eyebrow-text">Currently available for freelance<br>worldwide</span>
                <span class="hero__eyebrow-arrow">&#8599;</span>
            </a>

            <h1 class="hero__title" aria-label="Creative Visual Designer">
                <span class="hero__line" data-title-line>Creative Visual</span>
                <span class="hero__line" data-title-line>Designer</span>
            </h1>

            <a href="#" class="hero__play" data-play>
                <svg class="hero__play-scribble" viewBox="0 0 80 60" aria-hidden="true">
                    <path d="M10 30 Q 20 10, 40 20 T 70 35" stroke="#c9ff2d" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
                    <path d="M65 30 L 70 35 L 65 40" stroke="#c9ff2d" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
                <span class="hero__play-btn">
                    <svg viewBox="0 0 32 32" fill="none">
                        <circle cx="16" cy="16" r="15" stroke="#ffffff" stroke-width="1" fill="none"/>
                        <path d="M13 11l9 5-9 5z" fill="#ffffff"/>
                    </svg>
                    <svg class="hero__play-ring" viewBox="0 0 32 32" fill="none" aria-hidden="true">
                        <circle cx="16" cy="16" r="15.5" stroke="#c9ff2d" stroke-width="0.7" stroke-dasharray="4 3"/>
                    </svg>
                </span>
                <span class="hero__play-label">
                    <span>Work</span>
                    <span>Process</span>
                </span>
            </a>
        </div>

        <!-- ========== MARQUEE ========== -->
        <div class="marquee" id="marquee">
            <div class="marquee__track" id="marquee-track">
                <div class="marquee__group">
                    <span class="marquee__item">&#10036; Website Design &amp; Logo</span>
                    <span class="marquee__item">&#10036; Business Branding</span>
                    <span class="marquee__item">&#10036; Mobile Application Design</span>
                    <span class="marquee__item">&#10036; UI/UX Mobile Design</span>
                    <span class="marquee__item">&#10036; Motion &amp; Interaction</span>
                </div>
                <div class="marquee__group" aria-hidden="true">
                    <span class="marquee__item">&#10036; Website Design &amp; Logo</span>
                    <span class="marquee__item">&#10036; Business Branding</span>
                    <span class="marquee__item">&#10036; Mobile Application Design</span>
                    <span class="marquee__item">&#10036; UI/UX Mobile Design</span>
                    <span class="marquee__item">&#10036; Motion &amp; Interaction</span>
                </div>
            </div>
        </div>
    </section>

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

</body>
</html>
JSbanner-portfolio-dark.js— GSAP animation timeline
(() => {
    const root = document.getElementById('portfolio');
    if (!root) return;

    // -------------------------------------------------------------------
    //  GENERATE VINYL CONCENTRIC RINGS (both loader + stage)
    // -------------------------------------------------------------------
    const loaderRingsG = document.getElementById('loader-rings');
    const stageRingsG = document.getElementById('stage-rings');

    // Loader vinyl: rings from 70 to 195
    for (let r = 70; r <= 195; r += 3) {
        const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        c.setAttribute('cx', '200');
        c.setAttribute('cy', '200');
        c.setAttribute('r', r);
        c.setAttribute('fill', 'none');
        c.setAttribute('stroke', r % 12 === 0 ? '#c9ff2d' : '#1a1a1a');
        c.setAttribute('stroke-width', r % 12 === 0 ? '0.4' : (r % 6 === 0 ? '0.8' : '0.3'));
        loaderRingsG.appendChild(c);
    }

    // Stage vinyl: rings from 50 to 380
    for (let r = 50; r <= 380; r += 4) {
        const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
        c.setAttribute('cx', '400');
        c.setAttribute('cy', '400');
        c.setAttribute('r', r);
        c.setAttribute('fill', 'none');
        c.setAttribute('stroke', '#1f1f20');
        c.setAttribute('stroke-width', r % 20 === 0 ? '1.2' : '0.4');
        c.setAttribute('opacity', r % 20 === 0 ? '0.7' : '0.35');
        stageRingsG.appendChild(c);
    }

    // -------------------------------------------------------------------
    //  SPLIT TITLE INTO CHARS
    // -------------------------------------------------------------------
    const titleLines = root.querySelectorAll('[data-title-line]');
    titleLines.forEach((line) => {
        const text = line.textContent;
        line.textContent = '';
        [...text].forEach((ch) => {
            const span = document.createElement('span');
            span.className = 'hero__char';
            if (ch === ' ') {
                span.classList.add('is-space');
                span.innerHTML = '&nbsp;';
            } else {
                span.textContent = ch;
            }
            line.appendChild(span);
        });
    });

    // -------------------------------------------------------------------
    //  REFERENCES
    // -------------------------------------------------------------------
    const loader = document.getElementById('loader');
    const loaderVinyl = document.getElementById('loader-vinyl');
    const loaderCounter = document.getElementById('loader-counter');
    const loaderLabel = document.getElementById('loader-label');

    const stageVinyl = document.getElementById('stage-vinyl');
    const portrait = document.getElementById('portrait');

    const magnetics = root.querySelectorAll('[data-magnetic]');
    const rails = root.querySelectorAll('[data-rail]');
    const socials = root.querySelectorAll('[data-social]');
    const eyebrow = root.querySelector('[data-eyebrow]');
    const titleChars = root.querySelectorAll('.hero__char');
    const playBtn = root.querySelector('[data-play]');
    const playRing = root.querySelector('.hero__play-ring');
    const playScribble = root.querySelector('.hero__play-scribble path');

    const marqueeTrack = document.getElementById('marquee-track');
    const dots = root.querySelectorAll('.rail__dots span');

    // -------------------------------------------------------------------
    //  INITIAL STATES
    // -------------------------------------------------------------------
    gsap.set(magnetics, { y: -20, opacity: 0 });
    gsap.set(rails, { x: (i, el) => el.closest('.rail--left') ? -30 : 30, opacity: 0 });
    gsap.set(socials, { x: 30, opacity: 0 });
    gsap.set(eyebrow, { y: 20, opacity: 0 });
    gsap.set(titleChars, { yPercent: 110, opacity: 0 });
    gsap.set(playBtn, { y: 30, opacity: 0 });
    gsap.set(portrait, { x: 120, opacity: 0, scale: 0.95 });
    gsap.set(stageVinyl, { scale: 0.4, opacity: 0, rotation: -30 });

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

    // Vinyl continuously spins while counter ticks
    const vinylSpin = gsap.to(loaderVinyl, {
        rotation: 360,
        duration: 2.5,
        repeat: -1,
        ease: 'none',
        transformOrigin: 'center center',
    });

    const p = { v: 0 };
    loaderTl.to(p, {
        v: 100,
        duration: 2.3,
        ease: 'power1.inOut',
        onUpdate: () => {
            loaderCounter.textContent = String(Math.floor(p.v)).padStart(3, '0');
        },
    });

    loaderTl.call(() => {
        loaderLabel.textContent = 'SESSION READY';
    });

    // Exit: vinyl expands/fades, label + counter fade up
    loaderTl.to([loaderCounter, loaderLabel], {
        y: -20, opacity: 0, duration: 0.4, ease: 'power3.in', stagger: 0.05,
    }, '+=0.25');

    loaderTl.to(loaderVinyl, {
        scale: 3.6,
        opacity: 0,
        duration: 1,
        ease: 'power3.inOut',
        onComplete: () => vinylSpin.kill(),
    }, '-=0.3');

    loaderTl.to(loader, {
        opacity: 0,
        duration: 0.4,
        ease: 'power2.inOut',
    }, '-=0.5');
    loaderTl.set(loader, { display: 'none' });

    // -------------------------------------------------------------------
    //  MAIN SCENE ENTRANCE
    // -------------------------------------------------------------------
    function playScene() {
        const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });

        // Nav + actions
        tl.to(magnetics, {
            y: 0, opacity: 1,
            duration: 0.6,
            stagger: 0.05,
        }, 0);

        // Left + right rails
        tl.to(rails, {
            x: 0, opacity: 1,
            duration: 0.7,
            stagger: 0.08,
        }, 0.25);

        // Socials stagger
        tl.to(socials, {
            x: 0, opacity: 1,
            duration: 0.5,
            stagger: 0.06,
        }, 0.4);

        // Stage vinyl scales + rotates in
        tl.to(stageVinyl, {
            scale: 1, opacity: 0.95, rotation: 0,
            duration: 1.5,
            ease: 'power4.out',
        }, 0.2);

        // Portrait slides in
        tl.to(portrait, {
            x: 0, opacity: 1, scale: 1,
            duration: 1.4,
            ease: 'power4.out',
        }, 0.5);

        // Eyebrow link
        tl.to(eyebrow, {
            y: 0, opacity: 1,
            duration: 0.7,
        }, 0.9);

        // Title chars stagger
        tl.to(titleChars, {
            yPercent: 0, opacity: 1,
            duration: 1.1,
            stagger: 0.02,
            ease: 'expo.out',
        }, 1);

        // Play button
        tl.to(playBtn, {
            y: 0, opacity: 1,
            duration: 0.8,
            ease: 'back.out(1.6)',
        }, 1.6);

        // Draw scribble
        tl.to(playScribble, {
            strokeDashoffset: 0,
            duration: 0.9,
            ease: 'power2.out',
        }, 1.8);

        // Start continuous animations
        tl.call(startContinuous, null, 2.1);
        tl.call(enableInteractions, null, 2.1);
    }

    // -------------------------------------------------------------------
    //  CONTINUOUS ANIMATIONS
    // -------------------------------------------------------------------
    let vinylRot = null;
    let playRingRot = null;
    let marqueeAnim = null;

    function startContinuous() {
        // Stage vinyl slow spin
        vinylRot = gsap.to(stageVinyl, {
            rotation: '+=360',
            duration: 60,
            repeat: -1,
            ease: 'none',
            transformOrigin: 'center center',
        });

        // Play button ring spin
        playRingRot = gsap.to(playRing, {
            rotation: 360,
            duration: 8,
            repeat: -1,
            ease: 'none',
            transformOrigin: 'center center',
        });

        // Marquee scroll (infinite)
        const groupWidth = marqueeTrack.querySelector('.marquee__group').offsetWidth;
        marqueeAnim = gsap.to(marqueeTrack, {
            x: -groupWidth,
            duration: 28,
            ease: 'none',
            repeat: -1,
            modifiers: { x: gsap.utils.unitize(x => parseFloat(x) % groupWidth) },
        });

        // Scroll-down line pulses
        const scrollLine = root.querySelector('.rail__scroll-line');
        if (scrollLine) {
            gsap.fromTo(scrollLine, { scaleY: 0.3 }, {
                scaleY: 1,
                duration: 1.6,
                yoyo: true,
                repeat: -1,
                ease: 'sine.inOut',
                transformOrigin: 'top',
            });
        }
    }

    // -------------------------------------------------------------------
    //  INTERACTIONS
    // -------------------------------------------------------------------
    function enableInteractions() {
        // ---- Magnetic elements ----
        magnetics.forEach((el) => {
            const strength = el.classList.contains('topnav__cta') ? 0.35
                : el.classList.contains('topnav__menu') ? 0.45
                : el.classList.contains('topnav__logo') ? 0.2
                : el.classList.contains('hero__eyebrow') ? 0.18
                : 0.25;
            el.addEventListener('mousemove', (e) => {
                const r = el.getBoundingClientRect();
                const cx = r.left + r.width / 2;
                const cy = r.top + r.height / 2;
                const dx = (e.clientX - cx) * strength;
                const dy = (e.clientY - cy) * strength;
                gsap.to(el, { x: dx, y: dy, duration: 0.4, ease: 'power3.out' });
            });
            el.addEventListener('mouseleave', () => {
                gsap.to(el, { x: 0, y: 0, duration: 0.6, ease: 'elastic.out(1, 0.4)' });
            });
        });

        // ---- Title char proximity + hover ----
        const hero = document.getElementById('hero');
        let mx = -9999, my = -9999;
        hero.addEventListener('mousemove', (e) => {
            mx = e.clientX; my = e.clientY;
        });
        hero.addEventListener('mouseleave', () => {
            mx = -9999; my = -9999;
            titleChars.forEach((c) => {
                gsap.to(c, { y: 0, duration: 0.5, ease: 'power3.out' });
            });
        });

        gsap.ticker.add(() => {
            if (mx < 0) return;
            titleChars.forEach((c) => {
                const r = c.getBoundingClientRect();
                if (r.width === 0) return;
                const cx = r.left + r.width / 2;
                const cy = r.top + r.height / 2;
                const dx = mx - cx;
                const dy = my - cy;
                const dist = Math.sqrt(dx * dx + dy * dy);
                const PROX = 160;
                if (dist < PROX) {
                    const s = 1 - dist / PROX;
                    gsap.set(c, { y: -s * 22 });
                } else {
                    gsap.set(c, { y: 0 });
                }
            });
        });

        titleChars.forEach((c) => {
            c.addEventListener('mouseenter', () => {
                gsap.to(c, { color: '#c9ff2d', duration: 0.2 });
            });
            c.addEventListener('mouseleave', () => {
                gsap.to(c, { color: '#ffffff', duration: 0.4 });
            });
        });

        // ---- Social icons: spring scale + rotate ----
        socials.forEach((s) => {
            s.addEventListener('mouseenter', () => {
                gsap.to(s, {
                    scale: 1.18,
                    rotation: -8,
                    duration: 0.4,
                    ease: 'back.out(2)',
                });
            });
            s.addEventListener('mouseleave', () => {
                gsap.to(s, {
                    scale: 1,
                    rotation: 0,
                    duration: 0.5,
                    ease: 'elastic.out(1, 0.4)',
                });
            });
        });

        // ---- Grid dots: wave ripple on hover ----
        dots.forEach((dot, i) => {
            dot.addEventListener('mouseenter', () => {
                dots.forEach((d, j) => {
                    const delay = Math.abs(j - i) * 0.04;
                    gsap.to(d, {
                        scale: 1.8,
                        delay,
                        duration: 0.25,
                        yoyo: true,
                        repeat: 1,
                        ease: 'power2.inOut',
                    });
                });
            });
        });

        // ---- Play button: ring speeds up + scribble redraws ----
        playBtn.addEventListener('mouseenter', () => {
            if (playRingRot) playRingRot.timeScale(4);
            gsap.fromTo(playScribble,
                { strokeDashoffset: 120 },
                { strokeDashoffset: 0, duration: 0.6, ease: 'power2.out' }
            );
        });
        playBtn.addEventListener('mouseleave', () => {
            if (playRingRot) playRingRot.timeScale(1);
        });

        // ---- Marquee pauses on hover ----
        const marqueeEl = document.getElementById('marquee');
        marqueeEl.addEventListener('mouseenter', () => {
            if (marqueeAnim) gsap.to(marqueeAnim, { timeScale: 0.2, duration: 0.4 });
        });
        marqueeEl.addEventListener('mouseleave', () => {
            if (marqueeAnim) gsap.to(marqueeAnim, { timeScale: 1, duration: 0.4 });
        });

        // ---- Portrait tilt + vinyl parallax on mouse ----
        let tx = 0, ty = 0, cxPos = 0, cyPos = 0;
        root.addEventListener('mousemove', (e) => {
            const r = root.getBoundingClientRect();
            tx = ((e.clientX - r.left) / r.width - 0.5) * 2;
            ty = ((e.clientY - r.top) / r.height - 0.5) * 2;
        });
        root.addEventListener('mouseleave', () => { tx = 0; ty = 0; });

        gsap.ticker.add(() => {
            cxPos += (tx - cxPos) * 0.05;
            cyPos += (ty - cyPos) * 0.05;
            gsap.set(portrait, {
                x: cxPos * 15,
                y: cyPos * 10,
                rotation: cxPos * 1.2,
            });
        });
    }
})();

More GSAP landing pages