Nahorniak Templates
База референсів шаблонів і компонентів
Назад до шаблону · Webfolio - Creative Agency & Portfolio Next.js Template
c240

svg-curtain-preloader

Лоадер·Шаблон: Webfolio - Creative Agency & Portfolio Next.js Template·Складність анімації: heavy·Адаптивний: Так

Файли-джерела

  • out/home-main/index.htmldiv.loader-wrap

Бібліотеки

gsap

Summary

Full-viewport intro screen with a LOADING headline split letter-by-letter. A single SVG path is morphed by GSAP from a curve-bottomed shape to a flat line, then the whole .loader-wrap translates up by -1500px and is set to display: none. Total runtime is roughly 3.5 seconds; the hero <header> slides in from y: 200 overlapping the last 1.5s of the preloader.

HTML structure (minimal)

<div class="loader-wrap">
  <svg viewBox="0 0 1000 1000" preserveAspectRatio="none">
    <path id="svg" d="M0,1005S175,995,500,995s500,5,500,5V0H0Z"></path>
  </svg>
  <div class="loader-wrap-heading">
    <div class="load-text">
      <span>L</span><span>o</span><span>a</span><span>d</span><span>i</span><span>n</span><span>g</span>
    </div>
  </div>
</div>

Key SCSS tokens

.loader-wrap {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: $main_bg;
  display: flex;
  align-items: center;
  justify-content: center;

  svg { position: absolute; inset: 0; width: 100%; height: 100%; fill: $main_bg; }
  .load-text { font-size: 80px; font-weight: 600; color: #fff; }
  .load-text span { display: inline-block; opacity: .15; transition: opacity .3s; }
}

Animation logic

// components/common/loader.jsx
const interval = setInterval(() => {
  if (typeof gsap !== 'undefined') {
    clearInterval(interval);
    const svg = document.getElementById('svg');
    const tl = gsap.timeline();
    const curve = 'M0 502S175 272 500 272s500 230 500 230V0H0Z';
    const flat  = 'M0 2S175 1 500 1s500 1 500 1V0H0Z';

    tl.to('.loader-wrap-heading .load-text , .loader-wrap-heading .cont',
          { delay: 1.5, y: -100, opacity: 0 });
    tl.to(svg, { duration: 0.5, attr: { d: curve }, ease: 'power2.easeIn' })
      .to(svg, { duration: 0.5, attr: { d: flat },  ease: 'power2.easeOut' });
    tl.to('.loader-wrap', { y: -1500 });
    tl.to('.loader-wrap', { zIndex: -1, display: 'none' });
    tl.from('header', { y: 200 }, '-=1.5');
    tl.from('header .container', { y: 40, opacity: 0, delay: 0.3 }, '-=1.5');
  }
}, 100);

Notable details

  • GSAP path-attr tweening (attr: { d: ... }) animates between two SVG path strings — gives a rubbery curtain reveal you cannot achieve with clip-path alone.
  • Polls every 100ms until the global gsap script (loaded beforeInteractive) is on window, then runs once. Avoids useEffect race against external Script tags.
  • Final timeline frames overlap with the hero entrance (tl.from('header', { y: 200 }, '-=1.5')) so the page feels continuous.

Use when

  • Cinematic agency / portfolio sites where a 3-second intro is acceptable.
  • When you load GSAP globally as a <Script beforeInteractive> and want to call it from a 'use client' component.

Caveats

  • 3.5s blocking entrance is heavy — disable for users with prefers-reduced-motion (template doesn't currently).
  • Once display: none, screenshot tools at 1440x900 see only the page underneath; full-page screenshot still captures the intro, however, so the screenshot pipeline should wait at least 4s before capturing.
  • If gsap isn't loaded globally, the polling interval runs forever and the loader never hides.