Skip to content
animorio
IT
All components

The spotlight nav

Navigation, SVG, Physics, HoverGSAPMediumCompressed weight: Demo 5.0 KBGSAP 28 KBFreeDownload the zip

A stage light hanging above the links lights the current one, with a warm beam and a pool of light behind the word. Pointing at another link the light swings over to it like a hanging lamp, going a little past and coming back, and each word lights up as the light passes over it; a click closes the beam in on the word and makes it flare.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The spotlight nav</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mozilla-headline@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the nav: a stage light hangs above the links and lights the current one; pointed at, another link draws the
         light, which swings over to it, and a press makes it the current one with the beam closing in on it -->
    <nav class="sl" aria-label="Main" data-nav>
      <svg class="sl__light" aria-hidden="true" data-light>
        <defs>
          <linearGradient id="sl-beam" gradientUnits="userSpaceOnUse" data-beam-paint>
            <stop offset="0" class="sl__beam-near"/>
            <stop offset="0.72" class="sl__beam-mid"/>
            <stop offset="1" class="sl__beam-far"/>
          </linearGradient>
          <radialGradient id="sl-pool">
            <stop offset="0" class="sl__pool-mid"/>
            <stop offset="0.6" class="sl__pool-half"/>
            <stop offset="1" class="sl__pool-edge"/>
          </radialGradient>
        </defs>
        <path fill="url(#sl-beam)" data-beam/>
        <ellipse fill="url(#sl-pool)" data-pool/>
      </svg>
      <ul class="sl__list" data-list>
        <li><a class="sl__link" href="#" aria-current="page" data-link>Shows</a></li>
        <li><a class="sl__link" href="#" data-link data-autoclic>Tickets</a></li>
        <li><a class="sl__link" href="#" data-link data-autoclic>Cast</a></li>
        <li><a class="sl__link" href="#" data-link data-autoclic>Visit</a></li>
      </ul>
      <!-- an invisible frame round the light and the links, for the library's picture of the nav -->
      <span class="sl__frame" aria-hidden="true" data-frame></span>
      <!-- the light itself: the bar it hangs from, the yoke, the can and its lens -->
      <svg class="sl__lamp" aria-hidden="true" data-lamp-art>
        <g data-lamp>
          <path class="lamp__yoke" d="M-17 14V0H17V14"/>
          <rect class="lamp__can" x="-13" y="5" width="26" height="34" rx="5"/>
          <rect class="lamp__band" x="-14.5" y="27" width="29" height="6" rx="2"/>
          <path class="lamp__sheen" d="M-8 10V29"/>
          <ellipse class="lamp__lens" cx="0" cy="39" rx="11" ry="3.4" data-lens/>
          <path class="lamp__bolt" d="M3 0L1.5 2.6H-1.5L-3 0L-1.5 -2.6H1.5Z"/>
        </g>
        <rect class="lamp__bar" data-bar height="5" rx="2.5"/>
        <path class="lamp__stem" data-stem/>
      </svg>
    </nav>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

12 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The spotlight nav” from Animorio and fit it into my site

What it does

On a black background four links, Shows, Tickets, Cast and Visit, almost dark; above them, hanging from a bar, a graphite stage light with its lens lit. On arrival the light comes on with a stutter, as stage lights do, while it swings in from one side and stops on the current link: a warm beam comes down from the lens, widens downwards and fades, behind the word lies an oval pool of light and the lit word is bright, with a glow. With the mouse on another link the light turns towards it with the inertia of a hanging lamp, goes a little past and comes back, and the words light up and dim as the light crosses them; the current link never goes fully dark. Leaving the row the light goes back to the current link. The click closes the beam in on the word and makes it flare, then opens it again with a bounce. At rest the light sways a little.

How it works

A pendulum

The light's angle is a spring towards the link's angle, taken with atan2 from the hinge to the middle of the word: every frame, in four small steps, the acceleration is SWING times the distance from the target minus DAMP times the speed, and with these values the light goes a little past and comes back. Two slow waves, of 0.35 and 0.25 degrees, are added to the target: the sway at rest.

The beam

From the lens, 39 pixels from the hinge along the light's direction, two edges leave at plus and minus SPREAD degrees from the direction, cut where they meet a line 16 pixels below the middle of the words: a quadrilateral filled with a gradient along the beam, from 30% near the lens to zero on the line.

The pool and the words

The pool is an ellipse centred where the middle ray of the beam meets the words, as wide as the beam there (with its radius between 44 and 110 pixels) and flattened. Each word's light is a bell of its distance from that point: its colour goes from dark to light (gsap.utils.interpolate) with a text-shadow glow, and the current word never drops below 0.42.

The click's focus

A timeline takes the opening to 55% and the light's strength to 1.45 in 0.18 seconds, then back with elastic.out(1, 0.45) in 0.9 seconds; the light's strength sets the opacity of the beam, the pool and the lens.

Keys, sizes and the preview

Tab onto a link (focus-visible) brings the light there and Enter chooses it; leaving the row with the pointer (after 0.3 seconds) or the focus brings the light back to the current link. The light hangs HANG pixels up, but never more than 36% of the row's width (at least 90), so on a phone the angles stay readable; a ResizeObserver and the window's resize measure again. The library's preview clicks the other links in turn.

Values to change

NameFileDefaultWhat it changes
SWINGscript.js90How hard the light is pulled towards its link: more is quicker.
DAMPscript.js8.5How quickly the swing dies down: less swings more.
SPREADscript.js8Half the opening of the beam, in degrees.
HANGscript.js150How high above the links the light hangs, in pixels.

Try them in the Playground

When to use it

For the nav of a theatre, a live music venue, a cinema, a festival, a show, a dance company or a museum with dark rooms.

On phones

The text goes from 19 to 30 pixels with the width and the light hangs lower over a narrow row (36% of its width, at least 90 pixels); each link is 51 pixels tall and a tap brings the light there and closes it in on the word.

With reduced motion

The light points at the link at once, without swinging or swaying, is on from the start with no stutter and the click neither closes in nor flares the beam; the words light up and go dark at once, following the light.

Pitfalls

  • Near the horizontal the tangent of the beam's edges blows up: the edges' angles are capped at 78 degrees, or the beam of a far link would become endless.
  • The middle of the pool is where the middle ray meets the words: half way between the two edges, with a slanted beam, falls further out, and the light would seem to miss the word.
  • The beam must fade to zero before the line where it ends, or a straight cut would show under the words: the gradient runs from the lens to the point where the middle ray touches that line.
  • The words' colours change every frame: they are mixed with gsap.utils.interpolate between the colours read from the CSS variables --dark and --lit, so changing those changes the light too.
  • A pendulum is done in small steps (four a frame) and with a step of at most 1/30 of a second: with a long step, after a tab comes back into view, the spring would fly off.

Libraries

GSAP 3.15.0, Mozilla Headline (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 5.0 KB; the libraries: GSAP 28 KB.

Testing

Tested on 4 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components