A cursor that reads the room
A custom cursor is the most argued-about thing you can put on a personal site. Half the people who notice it love it and the other half go looking for the setting to turn it off. I've made three. The middle one was the loudest, and the last one is the first I'd defend.
A portraitThese follow a mouse, so on a phone the real site leaves them off. Tap anywhere here to see what a mouse would see at that spot.
The ring
The first one shipped with the portfolio in July: a 28px ring with a hairline border at half opacity, trailing the mouse. Over anything clickable it grew and darkened. The first version did that by changing its width and height, which meant the browser re-laid it out on every hover, and it chased the pointer by a fixed 16% a frame, so it ran faster on a 120Hz screen than on a 60Hz one.
The rewrite in October kept the look and fixed the plumbing. The ring stays 28px and is scaled instead, and position, size and opacity are all eased in the same loop by an amount that depends on elapsed time, not frames:
const kp = 1 - Math.exp(-dt * 14), ks = 1 - Math.exp(-dt * 11);
x += (mx - x) * kp; s += (ts - s) * ks; o += (to - o) * ks;
When everything has arrived, the loop stops. A cursor that isn't moving shouldn't cost anything.
The concept dot
For a concept page in October I tried the full award-site version: a 14px dot in mix-blend-mode: difference, driven by GSAP's quickTo (0.35s, power3), that swells to 2.2× over links and opens into an 86px disc with a word in it over the big items. It was fun for about a minute. It is a lot of cursor. The page it lived on carried a note saying concept only, not live, and that's where it stayed.
The landing dot
The one that lives on the landing page took the good parts and turned the volume down. It's an 86px element that rests at scale(.163), which is the same 14px dot, but growing it is a transform, not a resize. It trails the mouse by 22% of the distance each frame and inverts whatever is under it.
- Over a link or a button it swells to
.36. No word, because a circle full of text would cover the very link you're reading. - Over a few large items it opens to full size with a mono label: Flip, Back, Download, Open, Hi. The card reads Back once it's turned over.
- Over a pastel card, inverting looked muddy, so it stops blending and takes the card's own hue, deepened to
hsl(h 70% 74%), so it still shows on its own background.
Because the cursor says Flip, the little Flip badge on each work card is hidden for mouse users. Keyboard focus and touch still show it, which is the kind of thing you only remember after you've broken it.
No mouse, no cursor
All three only switch on for (hover: hover) and (pointer: fine), and the landing one also ignores any pointermove that isn't from a mouse. On a phone your finger is the cursor, and a dot that appears where you last touched is just a smudge. With reduced motion the dot still changes shape, but it stops trailing and sits exactly on the pointer.
- Built
- July and October 2026
- Stack
- Plain JS, CSS blend modes, GSAP for the concept
- Lives at
- thomasalex.in
- Source
assets/cursor.js