Skip to content
animorio
IT
All components

The thermochromic bar

Navigation, Hover, PhysicsCanvas, GSAPAdvancedCompressed weight: Demo 10 KBGSAP 28 KBFreeDownload the zip

The header of a bathhouse printed on a sheet of thermochromic liquid crystal, the film of mood rings: cold it is black, warmed it turns red, gold, green, blue and violet. The pointer is a warm fingertip that leaves trails which cool away; under the current link a heater keeps the film hot, and choosing another link it slides there like an iron, with a rainbow tail.

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 thermochromic bar</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/familjen-grotesk@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the top bar of a bathhouse's site, printed on a sheet of black thermochromic film: the pointer warms it like a
         fingertip, and a heater under the current link keeps the film hot round the word -->
    <header class="bar" data-bar>
      <!-- the film: a heat simulation painted through the colours of the liquid crystal -->
      <canvas class="bar__film" aria-hidden="true" data-film></canvas>
      <canvas class="bar__film bar__film--veil" aria-hidden="true" data-veil></canvas>
      <span class="bar__gloss" aria-hidden="true"></span>

      <a class="brand" href="#" aria-label="Solvarm, home" data-brand>
        <svg class="brand__mark" viewBox="0 0 28 28" aria-hidden="true">
          <path class="brand__steam" d="M9.5 13.5c-1.6-1.6 1.6-3.2 0-4.8s1.6-3.2 0-4.8"/>
          <path class="brand__steam" d="M14 13.5c-1.6-1.6 1.6-3.2 0-4.8s1.6-3.2 0-4.8"/>
          <path class="brand__steam" d="M18.5 13.5c-1.6-1.6 1.6-3.2 0-4.8s1.6-3.2 0-4.8"/>
          <path d="M4.5 23.5c1.2-3.6 4.6-6 9.5-6s8.3 2.4 9.5 6z"/>
        </svg>
        <span class="brand__name">Solvarm</span>
      </a>

      <nav class="bar__strip" aria-label="Main" data-strip>
        <ul class="bar__list" data-list>
          <li><a class="bar__link" href="#" data-link data-autoclic><span>Saunas</span></a></li>
          <li><a class="bar__link" href="#" data-link data-autoclic><span>Cold baths</span></a></li>
          <li><a class="bar__link" href="#" aria-current="page" data-link><span>Treatments</span></a></li>
          <li><a class="bar__link" href="#" data-link data-autoclic><span>Memberships</span></a></li>
          <li><a class="bar__link" href="#" data-link data-autoclic><span>Journal</span></a></li>
        </ul>
      </nav>

      <a class="bar__cta" href="#" aria-label="Book a visit" data-cta><span>Book<span class="bar__more"> a visit</span></span></a>
    </header>
  </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.

30 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 thermochromic bar” from Animorio and fit it into my site

What it does

On a white background a glossy black bar, 60 pixels tall with rounded corners, like a sheet of laminated plastic: on the left the Solvarm brand, three wisps of steam over a stone, in the middle five light grey links (Saunas, Cold baths, Treatments, Memberships, Journal), on the right the cream Book a visit button. Under the current link the film is warm: the word sits in a violet and blue patch, ringed with green, gold and red fading into the black, like a finger resting on a liquid crystal thermometer. On arrival the patch comes up out of the black through every colour. With the mouse on the bar the pointer warms the film like a fingertip: where it rests a circle blooms from red to green, where it passes it leaves a red and gold trail that cools in a couple of seconds; the finger is less hot than the current link, which stays the only violet one. The link pointed at lights up and warms a little round its word, and the button warms all round too. At a click a small bloom of heat opens where it was pressed, and the heater of the current link slides towards the one chosen: lower and a little cooler on the way, it leaves a tail running from blue to green, gold and red, while the old patch cools back through the colours and the new one spreads into rings. With Tab each link gets a teal outline and warms up, Enter chooses it. Pointing at the brand the steam rises again, one wisp after the other.

How it works

The film is a grid of heat

Under the bar lies a grid of cells, one every 4 pixels, with the heat of each in a Float32Array. A hundred and eighty times a second, whatever the screen's frame rate (the time step is capped at 1/30 of a second), every cell moves towards the mean of its four neighbours (T += SPREAD · (sum of the neighbours − 4T)), loses COOL/180 of its heat and takes the fingertip's. It is the heat equation solved with finite differences; at the edges the missing neighbour is the cell itself, so no heat leaks out of the edge.

The crystal's colours

A table of 512 colours, made from the colours in :root, runs from cold to hottest: black up to 0.17, then a deep red rising out of the black, red at 0.3, gold at 0.45, green at 0.55, teal, blue, violet at 0.86 and black again past 1.1, as the real crystal does past its clearing point. The canvas is half the size of the bar: each of its pixels reads the heat between the four nearest cells, where it is warm it shifts a little with a still noise (so the edges of the colours wander) and takes its colour from the table; putImageData paints it and the page stretches it.

The heater under the link

Under the letters of the current link sits a rounded box with a soft edge (the distance from a rounded rectangle, faded over 5 pixels) that pulls the cells towards HOT at every step. The rings round it are not drawn: spreading and cooling make them. The box is measured in the strip's coordinates (offsetLeft, which ignores the scroll), so on a phone the heat travels with the word, and it fades out near the strip's edges.

The slide and the press

Choosing a link, GSAP takes the heater's place and width to the new one in SLIDE seconds with power3.inOut, and on the way lowers it to 55 per cent and weakens it to 80: the cells it crosses warm halfway, and a rainbow tail that cools is left behind. A press pulls the film towards the finger's heat with a gaussian bell 10 pixels wide.

The fingertip

Every frame the pointer's heat is laid along the way it went since the frame before, a bell every 4 pixels. The total grows with the square root of the length: a quick swipe leaves a fainter trail than a slow one, but it leaves one. Each cell takes heat in proportion to the difference between the finger and itself, as skin does: the finger never warms anything past itself, and its heat (0.72) is below the heater's, so where it rests the film reaches green, and on the current link it does not cool it. On a phone the finger comes from touchmove, which keeps coming while the strip scrolls.

Still when it is still

Every frame the film is compared with the one before: when no cell changes by more than 0.0012 for twelve frames in a row, and nothing moves, the loop leaves GSAP's ticker and the canvas stays as it is; a gesture wakes it. A step costs 0.03 milliseconds and a paint 0.2. With less motion the settled film is worked out in one go (900 steps) and changing link is a fade over a second canvas.

Values to change

NameFileDefaultWhat it changes
HOTscript.js0.9How hot the film is under the current link: 0.3 red, 0.45 gold, 0.55 green, 0.75 blue, 0.9 violet, past 1 dark again.
WARMscript.js0.4How hot a link gets when pointed at or reached with Tab, and the button.
FINGERscript.js20How much heat goes from the pointer into the film: higher, brighter trails and bigger rings.
SPREADscript.js0.07How fast heat spreads through the film: higher, wider and softer rings.
COOLscript.js0.95How fast the film goes back to black: higher, shorter trails.
SLIDEscript.js0.7The seconds the heater takes to slide to the link chosen.
PRESSscript.js1How much heat a click or a tap leaves where it lands (0, none).

Try them in the Playground

When to use it

For the header of anyone selling warmth or well-being: saunas, bathhouses, hot springs, spas, coffee roasters, ceramics studios with a kiln, makers of stoves or thermal clothing. It also suits a dark tech site (sensors, thermal cameras, hardware) that wants a bar people remember. It is a bar to show off: one per site, at the top, on a light or a dark page.

On phones

The bar is 56 pixels tall and as wide as the screen less 32; the brand keeps its mark without the name and the button just says Book (screen readers still hear Book a visit). The links scroll sideways inside the bar, which fades out on the side where more are hidden; the chosen link slides to the middle and the heat follows it. The finger warms the film while it touches and while it drags the strip, and a tap presses and chooses at once.

With reduced motion

No fingertip, no trails and no slide: the film appears already settled round the current link. Choosing another link, the old film fades away over the new one, already warm round the word chosen; on a phone, scrolling the strip, the heat follows the word at once. Pointing at a link only lights it up, the brand's steam does not rise and presses do not shrink anything.

Pitfalls

  • A heater as tall as the link (40 pixels in a 60 pixel bar) made rings cut off at the top and bottom, a coloured column more than a finger on the crystal: now it sits on the letters, and slower spreading keeps the rings inside the bar.
  • After a click the mouse stays on the link, and its heat added to the heater's went past the clearing point: a black hole opened under the word. Now the finger warms in proportion to the difference between itself and the film, as skin does, and a press pulls towards that heat instead of adding to it: it never goes past.
  • With the finger as hot as the heater, the resting pointer left a violet disc with a rainbow ring: it looked like a glowing dot under the cursor and competed with the current link. Now the finger is cooler than the heater and reaches green: violet belongs only to the page you are on.
  • With the pointer's heat divided by the length of its way, a normal mouse movement left almost nothing: the total grows with the square root of the length, and the trail shows when moving fast too.
  • Watching the spreading is not enough to know the film is still: at rest the heater puts back at every step the heat the spreading takes from it. The film is compared with the one of the frame before, and the loop sleeps after twelve calm frames.
  • Choosing a link during the entrance, killTweensOf on the heater also stopped its warming up, which stayed off: now only its place, width, strength and height are stopped.
  • With less motion the loop does not run, and on a phone, scrolling the strip, the patch stayed put while the word went away: on every frame of scrolling the film settles again from where it is, in 160 steps.
  • On a phone pointer events stop (pointercancel) as soon as the finger starts scrolling the strip: the finger is followed with touchmove, which keeps coming.

Libraries

GSAP 3.15.0, Familjen Grotesk (font variabile) 5.3.0.

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

Testing

Tested on 5 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