SVG Editorial Spotlight

An immersive standalone opener where SVG light, orbit lines and editorial copy form a reactive stage.

This experiment requires full-page rendering.

Open live demo

HTML

<div class="svg-editorial-spotlight" data-spotlight-root>
  <section class="spotlight-stage">
    <div class="spotlight-copy">
      <p class="spotlight-eyebrow">Immersive editorial opener</p>
      <p class="spotlight-kicker" data-spotlight-current>Phase 01: Signal</p>

      <h1>Turn a manifesto into a moving atmosphere</h1>

      <p class="spotlight-intro">
        This concept explores how an editorial statement can feel less like a static hero block and more like a live
        signal. SVG beams, orbit lines and a responsive glow turn the opening of the page into an active scene.
      </p>

      <a class="spotlight-cta" href="#spotlight-phases">Explore the sequence</a>
    </div>

    <div class="spotlight-visual" aria-hidden="true">
      <svg class="spotlight-svg" viewBox="0 0 1200 900" role="presentation">
        <defs>
          <linearGradient id="spotlightBeam" x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" stop-color="#6ef3ff" />
            <stop offset="50%" stop-color="#8aa6ff" />
            <stop offset="100%" stop-color="#ffb45f" />
          </linearGradient>

          <radialGradient id="spotlightGlow" cx="50%" cy="50%" r="60%">
            <stop offset="0%" stop-color="rgba(255,255,255,0.95)" />
            <stop offset="40%" stop-color="rgba(110,243,255,0.35)" />
            <stop offset="100%" stop-color="rgba(110,243,255,0)" />
          </radialGradient>

          <filter id="spotlightBlur">
            <feGaussianBlur stdDeviation="18" />
          </filter>
        </defs>

        <g class="spotlight-halo">
          <circle cx="720" cy="380" r="230" fill="url(#spotlightGlow)" filter="url(#spotlightBlur)" />
          <circle cx="720" cy="380" r="150" class="spotlight-disc" />
        </g>

        <g class="spotlight-orbits">
          <ellipse class="orbit orbit-a" cx="710" cy="395" rx="335" ry="220" />
          <ellipse class="orbit orbit-b" cx="720" cy="390" rx="250" ry="150" />
          <ellipse class="orbit orbit-c" cx="705" cy="400" rx="410" ry="290" />
        </g>

        <g class="spotlight-beams">
          <path class="beam beam-a" d="M1180 140C1030 185 960 215 885 280C790 360 755 430 630 560" />
          <path class="beam beam-b" d="M1090 760C940 675 865 625 790 560C700 485 660 415 560 265" />
          <path class="beam beam-c" d="M300 95C420 180 485 240 560 315C645 400 695 455 835 610" />
        </g>

        <g class="spotlight-nodes">
          <g class="node node-signal is-active" data-spotlight-node="signal">
            <circle cx="522" cy="316" r="11" />
            <circle cx="522" cy="316" r="28" class="node-ring" />
          </g>

          <g class="node node-bloom" data-spotlight-node="bloom">
            <circle cx="884" cy="296" r="11" />
            <circle cx="884" cy="296" r="28" class="node-ring" />
          </g>

          <g class="node node-release" data-spotlight-node="release">
            <circle cx="772" cy="612" r="11" />
            <circle cx="772" cy="612" r="28" class="node-ring" />
          </g>
        </g>
      </svg>
    </div>
  </section>

  <section class="spotlight-phases" id="spotlight-phases">
    <article class="spotlight-phase is-active" data-spotlight-phase="signal">
      <p class="spotlight-phase__eyebrow">Phase 01</p>
      <h2>Signal</h2>
      <p>
        The page opens in a calm but charged state. The beams stay soft, the glow is wide, and the copy reads like the
        first line of a manifesto.
      </p>
    </article>

    <article class="spotlight-phase" data-spotlight-phase="bloom">
      <p class="spotlight-phase__eyebrow">Phase 02</p>
      <h2>Bloom</h2>
      <p>
        As the reader advances, the SVG structure grows brighter and tighter. The atmosphere feels less static and more
        intentionally composed around the editorial claim.
      </p>
    </article>

    <article class="spotlight-phase" data-spotlight-phase="release">
      <p class="spotlight-phase__eyebrow">Phase 03</p>
      <h2>Release</h2>
      <p>
        The final phase relaxes into a wider spread of light and motion, giving the scene a sense of completion instead
        of ending on a hard stop.
      </p>
    </article>
  </section>
</div>

CSS

.svg-editorial-spotlight {
  --pointer-x: 50;
  --pointer-y: 50;
  --scroll-progress: 0;
  --stage-accent: #6ef3ff;
  --stage-accent-2: #ffb45f;
  max-width: 1220px;
  margin: 0 auto;
  color: #f5f7fb;
}

.spotlight-stage {
  position: relative;
  min-height: 86vh;
  display: grid;
  align-items: end;
  padding: 42px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 34px;
  overflow: hidden;
  background:
    radial-gradient(circle at calc(var(--pointer-x) * 1%) calc(var(--pointer-y) * 1%), rgba(110, 243, 255, 0.14), transparent 28%),
    radial-gradient(circle at 78% 18%, rgba(255, 180, 95, 0.16), transparent 22%),
    linear-gradient(135deg, #070c14 0%, #0d1324 38%, #120d17 100%);
  box-shadow: 0 28px 90px rgba(0, 0, 0, 0.34);
}

.spotlight-stage::before,
.spotlight-stage::after {
  content: "";
  position: absolute;
  inset: auto;
  pointer-events: none;
  border-radius: 999px;
  filter: blur(24px);
}

.spotlight-stage::before {
  width: 280px;
  height: 280px;
  top: calc(10% + (var(--pointer-y) * 0.16%));
  left: calc(52% + (var(--pointer-x) * 0.08%));
  background: rgba(110, 243, 255, 0.16);
  animation: spotlightFloat 10s ease-in-out infinite;
}

.spotlight-stage::after {
  width: 240px;
  height: 240px;
  right: 8%;
  bottom: 12%;
  background: rgba(255, 180, 95, 0.14);
  animation: spotlightFloat 13s ease-in-out infinite reverse;
}

.spotlight-copy {
  position: relative;
  z-index: 2;
  max-width: 520px;
}

.spotlight-eyebrow,
.spotlight-phase__eyebrow {
  margin: 0 0 12px;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: rgba(255, 255, 255, 0.6);
}

.spotlight-kicker {
  margin: 0 0 18px;
  color: var(--stage-accent);
  font-weight: 700;
  letter-spacing: 0.04em;
}

.spotlight-copy h1 {
  margin: 0;
  max-width: 10ch;
  font-size: clamp(3.4rem, 8vw, 6.4rem);
  line-height: 0.9;
  text-wrap: balance;
}

.spotlight-intro {
  margin: 20px 0 0;
  max-width: 42ch;
  font-size: 1.08rem;
  line-height: 1.8;
  color: rgba(245, 247, 251, 0.82);
}

.spotlight-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 28px;
  padding: 12px 18px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: #ffffff;
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(10px);
}

.spotlight-cta:hover {
  border-color: rgba(110, 243, 255, 0.4);
}

.spotlight-visual {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.spotlight-svg {
  width: 100%;
  height: 100%;
}

.spotlight-disc {
  fill: rgba(255, 255, 255, 0.08);
  transform-origin: 720px 380px;
  transform: scale(calc(1 + (var(--scroll-progress) * 0.0007)));
}

.spotlight-orbits,
.spotlight-beams,
.spotlight-nodes {
  transform: translate(
    calc((var(--pointer-x) - 50) * 0.75px),
    calc((var(--pointer-y) - 50) * 0.55px)
  );
  transform-origin: 720px 380px;
  transition: transform 180ms ease-out;
}

.orbit {
  fill: none;
  stroke: rgba(255, 255, 255, 0.16);
  stroke-width: 1.2;
  stroke-dasharray: 12 16;
  animation: orbitSpin 26s linear infinite;
}

.orbit-b {
  stroke: rgba(110, 243, 255, 0.22);
  animation-duration: 18s;
  animation-direction: reverse;
}

.orbit-c {
  stroke: rgba(255, 180, 95, 0.2);
  animation-duration: 32s;
}

.beam {
  fill: none;
  stroke: url(#spotlightBeam);
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 0.52;
  stroke-dasharray: 10 18;
  animation: beamFlow 6s linear infinite;
}

.beam-b {
  animation-duration: 8s;
}

.beam-c {
  animation-duration: 7.2s;
}

.node circle:first-child {
  fill: #ffffff;
  opacity: 0.92;
}

.node-ring {
  fill: none;
  stroke: rgba(255, 255, 255, 0.32);
  stroke-width: 1.6;
  animation: nodePulse 3.6s ease-out infinite;
}

.node {
  opacity: 0.26;
  transition: opacity 220ms ease;
}

.node.is-active {
  opacity: 1;
}

.node-signal.is-active circle:first-child {
  fill: #6ef3ff;
}

.node-bloom.is-active circle:first-child {
  fill: #8aa6ff;
}

.node-release.is-active circle:first-child {
  fill: #ffb45f;
}

.svg-editorial-spotlight[data-active-phase="signal"] {
  --stage-accent: #6ef3ff;
  --stage-accent-2: #8aa6ff;
}

.svg-editorial-spotlight[data-active-phase="bloom"] {
  --stage-accent: #8aa6ff;
  --stage-accent-2: #ff7cbf;
}

.svg-editorial-spotlight[data-active-phase="release"] {
  --stage-accent: #ffb45f;
  --stage-accent-2: #ffe36a;
}

.spotlight-phases {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
  margin-top: 18px;
}

.spotlight-phase {
  padding: 24px;
  min-height: 260px;
  border-radius: 24px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent 60%),
    rgba(9, 10, 12, 0.96);
  transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
}

.spotlight-phase.is-active {
  transform: translateY(-4px);
  border-color: rgba(110, 243, 255, 0.32);
  box-shadow: 0 18px 44px rgba(0, 0, 0, 0.18);
}

.spotlight-phase h2 {
  margin: 0 0 12px;
  font-size: 2rem;
  line-height: 0.96;
}

.spotlight-phase p:last-child {
  margin: 0;
  color: rgba(245, 247, 251, 0.8);
  line-height: 1.75;
}

@keyframes orbitSpin {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

@keyframes beamFlow {
  from {
    stroke-dashoffset: 0;
  }

  to {
    stroke-dashoffset: -220;
  }
}

@keyframes nodePulse {
  0% {
    transform: scale(0.72);
    opacity: 0.7;
  }

  100% {
    transform: scale(1.3);
    opacity: 0;
  }
}

@keyframes spotlightFloat {
  0%,
  100% {
    transform: translate3d(0, 0, 0);
  }

  50% {
    transform: translate3d(12px, -16px, 0);
  }
}

@media only screen and (max-width: 980px) {
  .spotlight-stage {
    min-height: 74vh;
    padding: 28px;
  }

  .spotlight-copy h1 {
    max-width: 12ch;
  }

  .spotlight-phases {
    grid-template-columns: 1fr;
  }
}

JavaScript

(function () {
  const root = document.querySelector('[data-spotlight-root]');
  if (!root) return;

  const phases = Array.from(root.querySelectorAll('[data-spotlight-phase]'));
  const nodes = Array.from(root.querySelectorAll('[data-spotlight-node]'));
  const currentLabel = root.querySelector('[data-spotlight-current]');
  if (!phases.length || !nodes.length || !currentLabel) return;

  let pointerX = 50;
  let pointerY = 50;
  let targetX = 50;
  let targetY = 50;

  function setActivePhase(phase) {
    const phaseName = phase.dataset.spotlightPhase;
    const phaseTitle = phase.querySelector('h2');

    root.dataset.activePhase = phaseName;

    phases.forEach(item => {
      item.classList.toggle('is-active', item === phase);
    });

    nodes.forEach(node => {
      node.classList.toggle('is-active', node.dataset.spotlightNode === phaseName);
    });

    if (phaseTitle) {
      const phaseIndex = phases.indexOf(phase) + 1;
      currentLabel.textContent = `Phase 0${phaseIndex}: ${phaseTitle.textContent}`;
    }
  }

  function updatePointer(event) {
    const rect = root.getBoundingClientRect();

    targetX = ((event.clientX - rect.left) / rect.width) * 100;
    targetY = ((event.clientY - rect.top) / rect.height) * 100;
  }

  function resetPointer() {
    targetX = 50;
    targetY = 50;
  }

  function animatePointer() {
    pointerX += (targetX - pointerX) * 0.08;
    pointerY += (targetY - pointerY) * 0.08;

    root.style.setProperty('--pointer-x', pointerX.toFixed(2));
    root.style.setProperty('--pointer-y', pointerY.toFixed(2));

    requestAnimationFrame(animatePointer);
  }

  function updateScrollProgress() {
    const rect = root.getBoundingClientRect();
    const total = rect.height + window.innerHeight;
    const seen = Math.min(Math.max(window.innerHeight - rect.top, 0), total);
    const progress = total > 0 ? (seen / total) * 100 : 0;

    root.style.setProperty('--scroll-progress', progress.toFixed(2));
  }

  const observer = new IntersectionObserver(
    entries => {
      let nextPhase = null;
      let highestRatio = 0;

      entries.forEach(entry => {
        if (entry.intersectionRatio > highestRatio) {
          highestRatio = entry.intersectionRatio;
          nextPhase = entry.target;
        }
      });

      if (nextPhase) {
        setActivePhase(nextPhase);
      }
    },
    {
      threshold: [0.25, 0.45, 0.65]
    }
  );

  phases.forEach(phase => observer.observe(phase));

  root.addEventListener('pointermove', updatePointer);
  root.addEventListener('pointerleave', resetPointer);
  window.addEventListener('scroll', updateScrollProgress, { passive: true });

  setActivePhase(phases[0]);
  updateScrollProgress();
  animatePointer();
})();
  • Tech used Vanilla JS, WordPress-ready, No dependencies
  • Integration level Advanced (custom logic)
  • Performance safe Yes

What it solves

Creates a stronger sense of spectacle and brand presence for flagship editorial moments without needing canvas, video or external dependencies.

SVG Editorial Spotlight is a more theatrical experiment than the previous Nebula patterns. Instead of focusing on utility chrome or reading assistance, it turns the opening of a page into a moving editorial scene built around animated SVG beams, orbit lines and a responsive glow.

The goal is to explore how a manifesto, campaign intro or premium feature opener can feel immersive without relying on heavy libraries or video. Motion, atmosphere and hierarchy all come from CSS, SVG and a small layer of JavaScript.