VibeFX
Live preview — fully interactive

The prompt

Build a full-viewport hero section with an interactive canvas particle field and a staggered headline entrance.

## Structure
- A fixed-position <canvas> element behind a centered hero container (100vh)
- Hero content: small uppercase eyebrow label, two-line H1, one-sentence subtitle, two buttons (primary filled, ghost outlined)

## Particle system (vanilla JS canvas)
- ~110 particles (scale count with viewport width), each a small violet dot (rgba(124,108,255,0.7), radius 0.6-2.2px)
- Particles drift slowly (velocity ±0.35px/frame) with 0.985 damping
- Cursor repulsion: within 120px radius, push particles away with force proportional to proximity
- Draw connective lines between particles closer than 110px, opacity fading with distance (max 0.14)
- Respawn particles that leave the viewport; handle window resize

## Entrance animation (GSAP)
- Each headline line is wrapped in an overflow-hidden mask; inner spans start at translateY(110%)
- Animate spans to y:0 with power4.out, 1.1s, stagger 0.12s, 0.2s delay
- Fade in subtitle at 0.8s, CTA buttons at 1.05s

## Design tokens
- Background #08080d, text #f2f2f7, muted #8a8a99, accent #7c6cff
- Headline: clamp(40px, 7vw, 88px), weight 700, letter-spacing -0.03em, line-height 1.05
- Buttons: 14px 28px padding, 10px radius, 15px/600 type

## Constraints
- Single self-contained HTML file, GSAP 3.12 via cdnjs CDN, no other dependencies
- Must run at 60fps on a laptop; keep particle count adaptive

The code

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { background: #08080d; color: #f2f2f7; font-family: 'Inter', system-ui, sans-serif; overflow: hidden; }
  #field { position: fixed; inset: 0; }
  .hero { position: relative; z-index: 2; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 24px; }
  .eyebrow { font-size: 13px; letter-spacing: 0.2em; text-transform: uppercase; color: #7c6cff; margin-bottom: 20px; }
  h1 { font-size: clamp(40px, 7vw, 88px); line-height: 1.05; font-weight: 700; letter-spacing: -0.03em; }
  h1 .line { display: block; overflow: hidden; }
  h1 .line span { display: inline-block; transform: translateY(110%); }
  .sub { margin-top: 24px; max-width: 520px; color: #8a8a99; font-size: 17px; line-height: 1.6; opacity: 0; }
  .cta { margin-top: 36px; display: flex; gap: 14px; opacity: 0; }
  .btn { padding: 14px 28px; border-radius: 10px; font-size: 15px; font-weight: 600; text-decoration: none; }
  .btn-primary { background: #7c6cff; color: #fff; }
  .btn-ghost { border: 1px solid #23232e; color: #f2f2f7; }
</style>
</head>
<body>
<canvas id="field"></canvas>
<section class="hero">
  <p class="eyebrow">VibeFX Library</p>
  <h1>
    <span class="line"><span>Ship animated sites</span></span>
    <span class="line"><span>in minutes, not days</span></span>
  </h1>
  <p class="sub">Copy one prompt. Paste it into your AI builder. Get this exact hero — particles, motion and all.</p>
  <div class="cta">
    <a class="btn btn-primary" href="#">Browse the library</a>
    <a class="btn btn-ghost" href="#">See how it works</a>
  </div>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
  // ---- Particle field ----
  const canvas = document.getElementById('field');
  const ctx = canvas.getContext('2d');
  let W, H, particles = [];
  const mouse = { x: -9999, y: -9999 };
  const COUNT = Math.min(110, Math.floor(window.innerWidth / 12));

  function resize() {
    W = canvas.width = window.innerWidth;
    H = canvas.height = window.innerHeight;
  }
  resize();
  window.addEventListener('resize', resize);
  window.addEventListener('mousemove', (e) => { mouse.x = e.clientX; mouse.y = e.clientY; });
  window.addEventListener('mouseleave', () => { mouse.x = -9999; mouse.y = -9999; });

  class P {
    constructor() { this.reset(); }
    reset() {
      this.x = Math.random() * W; this.y = Math.random() * H;
      this.vx = (Math.random() - 0.5) * 0.35; this.vy = (Math.random() - 0.5) * 0.35;
      this.r = Math.random() * 1.6 + 0.6;
    }
    step() {
      const dx = this.x - mouse.x, dy = this.y - mouse.y;
      const d2 = dx * dx + dy * dy;
      if (d2 < 120 * 120) {
        const d = Math.sqrt(d2) || 1;
        const f = (120 - d) / 120 * 0.6;
        this.vx += (dx / d) * f; this.vy += (dy / d) * f;
      }
      this.vx *= 0.985; this.vy *= 0.985;
      this.x += this.vx; this.y += this.vy;
      if (this.x < -20 || this.x > W + 20 || this.y < -20 || this.y > H + 20) this.reset();
    }
    draw() {
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(124,108,255,0.7)';
      ctx.fill();
    }
  }
  for (let i = 0; i < COUNT; i++) particles.push(new P());

  function tick() {
    ctx.clearRect(0, 0, W, H);
    for (const p of particles) { p.step(); p.draw(); }
    // connective lines
    for (let i = 0; i < particles.length; i++) {
      for (let j = i + 1; j < particles.length; j++) {
        const a = particles[i], b = particles[j];
        const dx = a.x - b.x, dy = a.y - b.y;
        const d2 = dx * dx + dy * dy;
        if (d2 < 110 * 110) {
          ctx.strokeStyle = 'rgba(124,108,255,' + (0.14 * (1 - Math.sqrt(d2) / 110)) + ')';
          ctx.lineWidth = 1;
          ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
        }
      }
    }
    requestAnimationFrame(tick);
  }
  tick();

  // ---- Entrance ----
  gsap.to('h1 .line span', { y: 0, duration: 1.1, ease: 'power4.out', stagger: 0.12, delay: 0.2 });
  gsap.to('.sub', { opacity: 1, y: 0, duration: 0.9, delay: 0.8 });
  gsap.to('.cta', { opacity: 1, duration: 0.9, delay: 1.05 });
</script>
</body>
</html>

About this effect

A full-viewport hero section built on a lightweight canvas particle system. Particles drift slowly, connect with lines when close, and repel from the visitor's cursor. The headline enters with a masked line-by-line GSAP stagger. Perfect for AI, SaaS and developer-tool landing pages.

Interactive Particle Hero — Hero effect · VibeFX