
The elastic button
Buttons, Hover, Text
NewFree
The top bar of a small type foundry, in the minimal style: two hairline rules and a line of links that always fills the same measure, like a justified line. Point at a link and it widens and takes weight on the font's width axis, and the others narrow just enough for the line to stay exactly as long, each on its own spring. The page you are on stays a little wider and a thin vermilion tick comes down over it from the rule. On a phone the links open in a column that keeps its height the same way.
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 justified nav</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the top bar of a small type foundry, Tympan. Its links fill the same measure every time, like a justified line:
the link under the pointer widens and takes weight on the font's width axis, the others narrow just enough for
the line to stay exactly as long. The current page stays a little wider and a thin vermilion tick hangs over
it from the top rule. On a phone the links open in a column that keeps its height the same way -->
<header class="bar" data-bar data-autoclic>
<span class="bar__rule bar__rule--top" aria-hidden="true" data-rule></span>
<span class="bar__rule bar__rule--bottom" aria-hidden="true" data-rule></span>
<a class="brand" href="#" aria-label="Tympan, home" data-brand><span class="brand__word">Tympan</span></a>
<button class="toggle" type="button" aria-expanded="false" aria-controls="tympan-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 7.5h14" data-line/>
<path class="toggle__line" d="M3 12.5h14" data-line/>
</svg>
</button>
<nav class="nav" id="tympan-links" aria-label="Main" data-nav>
<span class="nav__end nav__end--start" aria-hidden="true" data-end></span>
<span class="nav__end nav__end--end" aria-hidden="true" data-end></span>
<ul class="list" data-list>
<li class="item"><a class="link" href="#" aria-current="page" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Typefaces</span></a></li>
<li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Trials</span></a></li>
<li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Licensing</span></a></li>
<li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Custom</span></a></li>
<li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Journal</span></a></li>
<li class="item"><a class="link" href="#" data-link><span class="link__mark" aria-hidden="true"><span class="link__tick" data-tick></span></span><span class="link__word" data-word>Studio</span></a></li>
</ul>
</nav>
</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>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.
23 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The justified nav” from Animorio and fit it into my siteRoboto Flex has a width axis from 25 to 151 and a weight axis from 100 to 1000. At start and whenever the sizes change the script gives every word, one setting after another, the values of a grid (six widths by four weights) and reads getBoundingClientRect().width each time: 24 reads in all. Between the points of the grid the width comes from two straight-line interpolations, and nothing is measured while things move.
Every link has an extra width (HOVER_WIDTH under the pointer, CURRENT_WIDTH if it is the current page) and a weight. Every frame a 28-step bisection looks for the base width b at which the words, each at b plus its own extra, add up to exactly the measure less the spaces (GAP for each space). The list is a flex row with justify-content: space-between, so the first word sits at the start and the last at the end: if the axis reaches the end of its range, the space between the words takes up the rest, as in a line of type.
How much a link is under the pointer and how much it is the current page go from 0 to 1 on damped springs (SPRING), with steps of at most 1/120 of a second and never more than 1/30 a frame: the word that widens goes a little too far and comes back. Every word's base also follows the bisection's on a spring of its own, softer the further it is from the link that changed (RIPPLE): the neighbours narrow at once, the far ones a moment later, and the spaces breathe meanwhile. If the springs together made the line too long, a second bisection takes the same width off all of them until the spaces no longer close below the minimum.
What counts is not the word under the pointer but the middle of the space: the line is cut halfway through every space between two words. A word that widens under the pointer carries its edges with it, so it keeps the pointer until the pointer is past the middle of the next space: no flicking back and forth between two links as the line moves. Off the bar the wide word narrows again; the keyboard focus does the same as the pointer.
Every link has a vermilion tick 2 pixels wide hanging from the top rule over the middle of the word: one element for the position (left: 50%, from the CSS) and its child for GSAP, which brings it down with scaleY and a back.out (a pixel too far and back) and takes it up with a power2.in. The tick lives inside the link, so it follows the word while the line justifies itself. The chosen link takes aria-current="page".
Below 760 pixels the same links go into a panel under the bar, which the Menu button opens with a clip-path coming down. The panel's height never changes (six rows of 46): the row under the finger grows ROW_GROW pixels taller and the others give up the room, the current page keeps a third of it; the row's type grows with it, widens and takes weight, and if it does not fit it first narrows and then gets smaller. A finger sliding down the column moves the tall row; lifting it chooses that link and the panel closes. The tick is the same one on its side: it comes out of the left edge of the row.
| Name | File | Default | What it changes |
|---|---|---|---|
HOVER_WIDTH | script.js | 56 | How much wider the link under the pointer gets, in units of the width axis (which goes from 25 to 151). |
HOVER_WEIGHT | script.js | 720 | The weight of the link under the pointer (100 to 1000). |
CURRENT_WIDTH | script.js | 10 | How much wider the page you are on stays, on the width axis. |
CURRENT_WEIGHT | script.js | 540 | The weight of the page you are on. |
REST_WEIGHT | script.js | 380 | The weight of the other links. |
GAP | script.js | 38 | The space the words keep between them, in pixels: the width axis takes up the rest. Bigger, narrower links. |
SPRING | script.js | [230, 22] | Every link's spring: how stiff, and how much it is held back. |
RIPPLE | script.js | 0.35 | How much later the links further from the one that changes answer (0, all together). |
ROW_GROW | script.js | 28 | On a phone, how many pixels taller the row under the finger gets (the others give up the room). |
For the header of anyone who works with type and words: a type foundry, a graphic design studio, a magazine, a publisher, the portfolio of a typographer or a designer. It suits five, six or seven short links on a light, empty bar; with very long links the axis soon reaches its end and only the spaces work. Colours and weights change in the variables.
Below 760 pixels the bar is 56 pixels tall and as wide as the screen less 32 (358 at 390): the name on the left, the Menu button with two thin lines on the right. Tapped, the lines turn into a cross, the word says Close and a white panel comes down under the bar with the six links in a column, divided by thin lines: the words come in one after the other, from narrow to their width, and the vermilion tick comes out of the left edge of the current page. The panel always keeps the same height: with a finger on a row it grows taller, its word turns big, wide and full, and the other rows narrow to make room; sliding the finger up and down, the tall row follows it. Lifting the finger chooses that link, the tick comes down on the new one and the panel closes. Esc closes and gives the focus back to the button.
No springs and no widening words: the link under the pointer only turns black, the current page is already a little wider and the line is already justified. Choosing another link the line is set again in place in a short fade and the vermilion tick fades out on the old one and back in on the new one, without coming down. When the page opens everything fades in; on a phone the panel fades in and out without coming down, and the rows keep the same height.
GSAP 3.15.0, Roboto Flex (font variabile, con gli assi della larghezza e del peso) 5.3.0.
Compressed with gzip, the three demo files weigh 9.8 KB; the libraries: GSAP 28 KB.
Tested on 16 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.