Snake
The phone-game snake, chasing the pointer square by square. It grows over controls and speeds up on press.
npx shadcn@latest add https://hoverui.fun/ r/ snake.jsonAdds components/cursors/snake.tsx. The CLI also installs the useCursor hook it runs on, once, into your hooks folder. The files are yours from there.
Move through the preview, then hover the controls and press.Open this on a computer to move it around: it follows a mouse, not a finger.
- Kind
- Retro
- Hook
- useCursor
- Dependencies
- None
- License
- MIT
How it behaves
The phone-game snake, chasing the pointer square by square on a 10px grid; it grows over anything clickable and speeds up on press.
Usage
import { SnakeCursor } from "@/components/cursors/snake";
export function Section() {
return (
<section>
<SnakeCursor />
{/* this section now wears Snake */}
</section>
);
}Put it inside <body> in your root layout to wear it on every page. Nested cursors take over inside their own element.
Props
- className
- Colors the cursor through currentColor, e.g. text-pink-500. To read on any background, pass text-white mix-blend-difference, as this site does.
- Hover targets
- a, button, input, select, textarea, label, [role=button], [data-cursor-label]
Source
"use client";
import { useRef } from "react";
import { cursorRoot, useCursor } from "@/hooks/use-cursor";
const CELL = 10;
const MAX = 24;
/** The phone-game snake, chasing the pointer square by square on a 10px grid; it grows over anything clickable and speeds up on press. */
export function SnakeCursor({ className = "" }: { className?: string }) {
const s = useRef({ cells: [] as number[][], steps: 0 });
const ref = useCursor((p, el) => {
const st = s.current;
const [gx, gy] = [Math.floor(p.x / CELL), Math.floor(p.y / CELL)];
const len = p.hover ? 16 : 8;
if (!p.dt || p.reduced || !st.cells.length) st.cells = Array.from({ length: len }, () => [gx, gy]);
// Never diagonal, the longer way first. It sprints when far behind and crawls near the pointer; a press doubles it.
const far = Math.abs(gx - st.cells[0][0]) + Math.abs(gy - st.cells[0][1]);
st.steps += p.dt * (30 + far * 3) * (p.down ? 2 : 1);
for (; st.steps >= 1; st.steps--) {
const [hx, hy] = st.cells[0];
if (hx === gx && hy === gy) {
st.steps = 0;
break;
}
const [dx, dy] = [gx - hx, gy - hy];
st.cells.unshift(Math.abs(dx) >= Math.abs(dy) ? [hx + Math.sign(dx), hy] : [hx, hy + Math.sign(dy)]);
}
// Growing adds squares at the tail; shrinking drops them.
while (st.cells.length < len) st.cells.push(st.cells[st.cells.length - 1]);
st.cells.length = len;
const els = el.children as HTMLCollectionOf<HTMLElement>;
for (let i = 0; i < MAX; i++) {
const cell = st.cells[i];
els[i].style.opacity = cell ? "1" : "0";
if (cell) els[i].style.transform = `translate(${cell[0] * CELL + 1}px, ${cell[1] * CELL + 1}px)`;
}
// The food is the exact pointer.
els[MAX].style.transform = `translate(${p.x}px, ${p.y}px)`;
});
return (
<div ref={ref} aria-hidden className={`${cursorRoot} ${className}`}>
{Array.from({ length: MAX }, (_, i) => (
<div key={i} className="absolute size-2 bg-current opacity-0" />
))}
<div className="absolute size-1 -translate-1/2 bg-current" />
</div>
);
}
It runs on the useCursor hook, which has its own source and API.
More retro cursors
- ClassicThe 1-bit desktop arrow, pixel for pixel: a pointing hand over controls, the hourglass while you press.
- ChompAn arcade chomper that faces your way and eats as it goes. Gobbles controls; snaps shut on press.
- MatrixGreen code rains off your path, pours over anything clickable, and bursts on click.
- PixelA pixel cursor on an 8px grid with a stepped, fading trail. Over controls it draws a blinking selection.
- CoinAn arcade coin on the pointer. Every click pops a spinning coin with a "+1" and keeps score; over a control a "?" block hangs above it and the click bumps it.
Made a cursor of your own? Contributed cursors carry their maker's name on their page.
Contribute a cursor