Heavier where you look
The headline on my portfolio is three short lines in a very thin typeface. When you move the pointer over it, the letters near the pointer put on weight, as if they were leaning in to listen. Move away and they go back to being thin.
It is one of those effects that only works because of a variable font. Hanken Grotesk ships as a single file with a weight axis that runs from 100 to 900, so the browser can draw any weight in between. Each letter is its own <span>, and each frame I set its font-weight to a number like 431.7. No font swaps, no extra files.
The first version
I wrote the first version in July 2026. On every mouse move it measured every letter, worked out how far the pointer was from its centre, and set the weight straight away: 100 at rest, rising in a straight line to 580 at the pointer, inside a 240px radius. The italic line started at 200. A CSS transition of .3s ease-out did the smoothing.
It looked fine in a screenshot. In the hand it shivered. A letter that gets heavier also gets wider, which pushes its neighbours along, which changes where their centres are, which changes how heavy they should be. Measuring layout on every move fed the effect back into itself. You can feel it on the July chip above if you hold the pointer still at the edge of a word: the line never quite settles.
Three small fixes
The October version, which went live on 17 October 2026, is the same idea with three changes.
- Cached centres. I measure every letter once, when the pointer arrives, and again only after everything has come to rest. While letters are changing weight, nobody reads the layout. Scrolling is handled by remembering the scroll position at measure time and offsetting by the difference.
- A smoothstep falloff. Instead of a straight line, the boost is
u*u*(3-2*u)whereuis how close the letter is, from 0 at 240px to 1 at the pointer. It is flat at both ends, so there is no visible edge where the effect starts. - An ease that doesn't care about frame rate. Each frame moves the weight a fraction
1-exp(-dt*9)of the way to its target. On a 60Hz screen that is about 14% a frame, on 120Hz about 7%, and both arrive at the same moment.
The range moved up too: 200 at rest, 680 at the pointer (300 to 780 for the italic line). At 100 the thinnest letters were hard to read on a cheap screen, which is a bad trade for a headline.
const k = 1 - Math.exp(-dt * 9); // frame-rate independent ease
for (let i = 0; i < n; i++) {
let hit = 0;
if (inside) {
const d = Math.hypot(mx - cx[i], my - (cy[i] + sy)) / R;
if (d < 1) { const u = 1 - d; hit = u * u * (3 - 2 * u); }
}
tgt[i] = base[i] + hit * 480;
cur[i] += (tgt[i] - cur[i]) * k;
letters[i].style.fontWeight = cur[i].toFixed(1);
}
The loop also stops itself. When every letter is within 0.15 of its target it cancels the animation frame, so a headline nobody is touching costs nothing.
On a phone
On the portfolio the effect is mouse only, and it switches off entirely if you ask your system for reduced motion. For this page I added touch: drag sideways across the words and they light up under your finger, then relax a moment after you let go. Vertical drags still scroll the page, because nothing here is worth stealing your scroll for. The headline here is also much smaller than on the portfolio (where it is about 94px tall at a laptop size), so the 240px radius shrinks with the type to keep the same proportions. With reduced motion on, the demo skips the easing and simply sets each weight.
- Built
- July 2026, reworked October 2026
- Stack
- Variable font, a span per letter, requestAnimationFrame
- Lives at
- thomasalex.in/portfolio
- Source
setupKinetic()in portfolio.html