Skip to content
animorio
IT
All components

The stained glass nav

Navigation, Hover, Physics, ImagesCanvas, GSAPAdvancedCompressed weight: Demo 20 KBGSAP 28 KBFreeDownload the zip

The top bar of a glass museum made as a leaded window: every link is a pointed lancet of coloured glass set in its lead, with the cords, the seeds and the darker painted edge of hand-blown glass. A warm light behind the bar shines through the tile of the page you are on and throws its colour on the floor under the bar, upside down and widening like the light of a church window; pointing at another link the light slides behind it on a spring. Glass, lead and floor light are worked out pixel by pixel.

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 stained glass nav</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/cinzel@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the top bar of a glass museum, made as a leaded window: every link is a lancet of coloured glass in its dark
         lead, with the grain, the cords and the seeds of hand-made glass. A warm light behind the bar shines through
         the tile of the page you are on and throws its colour on the floor under the bar, like the light on the floor
         of a church; pointing at another tile the light slides behind it. On a phone the links open in a second
         window, one row of glass each, and the light drops into it -->
    <div class="vitrail" data-vitrail data-autoclic>
      <header class="bar" data-bar>
        <canvas class="glass" aria-hidden="true" data-glass></canvas>

        <a class="brand" href="#" aria-label="Aldgrave Glass Museum, home" data-brand>
          <span class="brand__rondel" aria-hidden="true" data-rondel></span>
          <span class="brand__name">Aldgrave</span>
        </a>

        <nav class="nav" id="aldgrave-links" aria-label="Main" data-nav>
          <canvas class="nav__glass" aria-hidden="true" data-panel-glass></canvas>
          <ul class="list" data-list>
            <li class="item" data-glass-color="ruby"><a class="link" href="#" data-link><span class="link__word" data-text="Collection">Collection</span></a></li>
            <li class="item" data-glass-color="cobalt"><a class="link" href="#" aria-current="page" data-link><span class="link__word" data-text="Exhibitions">Exhibitions</span></a></li>
            <li class="item" data-glass-color="amber"><a class="link" href="#" data-link><span class="link__word" data-text="Studio">Studio</span></a></li>
            <li class="item" data-glass-color="emerald"><a class="link" href="#" data-link><span class="link__word" data-text="Learn">Learn</span></a></li>
            <li class="item" data-glass-color="violet"><a class="link" href="#" data-link><span class="link__word" data-text="Shop">Shop</span></a></li>
          </ul>
        </nav>

        <div class="end" data-end>
          <a class="cta" href="#" data-cta><span class="cta__bg" aria-hidden="true" data-cta-bg></span><span class="cta__label">Tickets</span></a>
        </div>

        <button class="toggle" type="button" aria-expanded="false" aria-controls="aldgrave-links" data-toggle>
          <span class="toggle__label" data-toggle-label>Menu</span>
          <svg class="toggle__icon" viewBox="0 0 20 20" aria-hidden="true">
            <path class="toggle__line" d="M3.5 7.5L16.5 7.5" data-line/>
            <path class="toggle__line" d="M3.5 12.5L16.5 12.5" data-line/>
          </svg>
        </button>
      </header>

      <!-- the light that comes through the glass, on the floor under the bar (and under the window of the phone) -->
      <canvas class="floor" aria-hidden="true" data-floor></canvas>
      <canvas class="floor floor--panel" aria-hidden="true" data-panel-floor></canvas>
    </div>
  </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.

38 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 stained glass nav” from Animorio and fit it into my site

What it does

On a white background a bar 64 pixels tall and up to 1000 wide, made as a leaded window: a dark lead frame with round corners and inside it seven lancets of glass side by side, each with the point of its arch touching the frame. On the left the name Aldgrave in dark Roman capitals, painted on pale antique glass beside a spun green gold rondel with its rings; then the five links on coloured glass: Collection ruby, Exhibitions cobalt, Studio amber, Learn emerald, Shop violet, with pale letters as if ground out of the colour and a dark line round them; at the end the dark Tickets button on more pale glass. Each coloured tile is cut in three by two slanted leads, one piece of a darker and one of a paler glass of the same colour, and over the arch there are two corners of pale glass; where the leads meet there is a dab of solder. The glass is not flat: it has the long cords of hand-drawn glass, a few opal wisps, seeds and a darker edge along the lead. On arrival the glass brightens like the morning and a warm light comes on behind Exhibitions: the tile lights up a full blue, its letters warm up and under the bar a pool of blue light spreads on the floor, with the shadow lines of the leads and the bright specks of the seeds, fading as it goes away. Pointing at another link the light slides behind it on a spring, goes a little past and comes back, and the pool on the floor changes colour with it; the page you are on stays a little lit and still throws a veil of its colour. Choosing a link the light stays there with a small flare. With Tab or the arrow keys the light follows the focus, shown by a gold ring round the word.

How it works

The window is a map of pieces

The script measures the links in the DOM and turns them into a plan: zones side by side (the name, the five tiles, the button), the arch of each lancet (y = F + RISE·(0.4u + 0.6(1 − √(1 − u))), u being the distance from the centre over the half width: pointed in the middle, nearly upright where it springs), the two slanted cuts of each tile kept clear of the word, the rondel beside the name. For every pixel of the canvas (up to 300,000) a function pieceAt(x, y) says which piece it belongs to. On a phone the plan is another window: coloured rows in a border of pale glass with gold squares.

The lead is a distance field

Every pixel that borders a different piece is a seed; the exact distance transform of Felzenszwalb and Huttenlocher (two passes of parabolas, columns then rows) gives every pixel its distance to the nearest edge, softened with two passes of blur so the pixel staircase on the slanted cuts does not leave the lead twisted like a rope. Where the distance is under 1.6 pixels there is lead; the gradient of the field gives the normal of its round profile, lit from above with diffuse light and a shine. The dabs of solder are circles joined to the lead with a smooth minimum, and the frame comes from the signed distance of a rounded rectangle.

The glass: thickness, cords, seeds

For each piece a fractal noise stretched along an angle of its own (scales of 30 and 6.5 pixels) makes the cords, a fine noise makes the hammering; their value is the thickness, how much light gets through, and their slope becomes a shift (RIPPLE): every pixel looks for the light at a slightly shifted point, so the light behind wobbles through the cords. The tiles have a few opal wisps and a darker painted edge along the lead; the seeds, about one for every 230 square pixels, are ellipses bright in the middle, where they focus the light, and dark at the rim. All this is worked out once for each size.

At every frame, a small shader in JavaScript

Every pixel of glass adds up the daylight, a little stronger at the top, and the warm light: an elliptical Gaussian centred behind the tile (read from a table of 1024 values), plus a second, weaker glow behind the current page when the light goes elsewhere. The colour of the glass times the thickness times the light goes through a tone curve (1 − e^(−1.6x), then the gamma) that rounds off the strong parts instead of clipping them; the lead is mixed over it by its coverage. The real letters, in the DOM, get --lit, how lit they are, for their colour and glow.

The light on the floor looks back

The floor is a canvas at half resolution under the bar. Each of its points, for each light, looks back through the window: row t of the floor matches row bottom − t·height of the window (the bottom lands near, the top far: the image is upside down), and x comes closer to the light by 1 + SPREAD·t, so the pool widens. The point takes the colour of the glass it passes through, its transparency and the light behind it; the lead leaves a line of shade. The lights add up like overlapping panes.

The spring of the light, in the space of the links

The light does not live in pixels but in the space of the links (1.5 is halfway between the second and the third): a spring with the stiffness GLIDE and the damping worked out from BOUNCE takes it to the link pointed at, in steps of 1/120 of a second with time capped at 1/30. So if the window changes size halfway, the glass is rebuilt and the light keeps its place. On the move the pool stretches with the speed. When the spring is still and no animation is running the loop stops and nothing is drawn.

Values to change

NameFileDefaultWhat it changes
GLIDEscript.js150How hard the spring pulls the light behind the link pointed at: higher, quicker.
BOUNCEscript.js0.1How far the light swings past the link and back, as a share of the way (0, not at all).
LIGHTscript.js2.2How strong the warm light behind the bar is.
DAYLIGHTscript.js0.3The dim daylight that comes through every piece of glass: higher, brighter glass even unlit.
FLAREscript.js0.45How much the light flares up when a page is chosen (0, not at all).
FLOORscript.js0.7How strong the coloured light thrown on the floor is (0, none).
SPREADscript.js0.8How much the light on the floor widens as it goes away from the bar.
SEEDSscript.js1How many seeds, the small bubbles of hand-made glass, are in the glass (0, none).
RIPPLEscript.js1How much the uneven glass bends the light behind it (0, flat glass).

Try them in the Playground

When to use it

For the header of anyone who works with glass, light or sacred art: a glass museum, a gallery, a glassworks or a stained glass studio, a glassblowing workshop, a church or abbey open to visitors, a festival of light. It suits a few short links, each with its own colour; the colours of the glass, the lead and the light are set in the variables.

On phones

Under 760 pixels the bar is 60 pixels tall and as wide as the screen minus 32 (358 at 390): on the left the name with its rondel, on the right the Menu button on a lancet of the colour of the page you are on, with the light behind it and its pool on the floor. Tapped, a second window drops under the bar with the five links in rows of coloured glass, in a border of pale glass with gold squares: the light falls from above into the row of the current page and its pool opens under the window. Sliding a finger along the rows the light follows it, and the row it lifts on is chosen; the glass of the button takes its colour and the window closes. Under 980 pixels the Tickets button goes.

With reduced motion

The light does not travel: pointing at or choosing another link it fades out where it was and fades in behind the new tile, with its pool on the floor. No flare, no words that shrink, the button does not widen. On a phone the window appears and goes with a fade, with the light already in place.

Pitfalls

  • The first arch, 1 − √(1 − u), on tiles 130 pixels wide and 54 tall was nearly a straight line along the top: the window looked like a row of buttons. The arch now has a linear part that makes it pointed, every tile is cut in three by two slanted leads and the frame has dabs of solder: it reads as stained glass at first sight.
  • On the slanted cuts the lead worked out from the distance transform was twisted like a rope: the seeds are a staircase of pixels and the gradient of the field changed direction at every step. Two passes of blur on the field before the shading make it smooth without widening it.
  • The pool on the floor was lavender grey instead of blue: its colour was the glass normalised and put through the gamma, which washed it out. Now it is the colour of the glass as it looks with the light behind it, through the same tone curve as the bar.
  • With the light behind another link, the glow of the current page threw a long crooked streak on the floor: the projection used the main light as the centre for both. Each light now looks back from its own centre, and their contributions add up.
  • At full light the green and the amber turned mint and cream: the white bloom of the strong parts started too early. It now starts only above 1.6 times full light and is weaker, and lit glass stays saturated.
  • On a phone the window cut itself off at the side while closing: the browser rewrites inset(0px 0px 0% 0px round 15px) with three values, and GSAP tweened the numbers in the wrong order. The closing now starts from a string written out in full.
  • The pool under the phone's window, 270 pixels tall, landed far away and squashed into a line: now the floor shows only the rows round the light, the lit one in the middle, and the pool starts right under the window.

Libraries

GSAP 3.15.0, Cinzel (font variabile, con l'asse del peso) 5.3.0, Mona Sans (font variabile, con l'asse della larghezza, per il pulsante) 5.3.0.

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

Testing

Tested on 17 October 2026

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

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