English · 简体中文 · 繁體中文 · 日本語 · 한국어 · Español · Français · Deutsch · Português (Brasil) · Русский
A dependency-free recreation of the block trail that follows the cursor on githubuniverse.com: when the pointer enters a new cell of a 20px grid, one block from a fixed pool is reused, gets a random brand color and ASCII glyph, and shrinks away after 100–400ms.
No build step, no dependencies, no framework — three static files plus one effect script.
Open index.html directly, or serve the folder:
python -m http.server 8000
# open http://localhost:8000The effect listens for mouse events, so touch devices will not trigger it.
| File | Purpose |
|---|---|
index.html |
Demo page: hero, parameter panel, explanation |
styles.css |
Page styles + the core trail CSS |
trail.js |
The effect, exposes createCursorTrail() |
demo.js |
Wires the parameter panel to trail.js |
- listens to
mousemoveonly on.trailAnimationAreaelements; - skips
.trailAnimationIgnoresubtrees,<a>,<button>, form controls and text-only leaf nodes; - rounds the pointer position to the
grid, and never fires twice for the same cell; - recycles
poolblocks, keeping the DOM size constant; - each activation picks a random color (6 brand colors) and glyph (11 ASCII characters, 30% chance of a blank);
- removes
is-activeafterminLife–maxLifems; CSS handles the shrink; - disables itself for
prefers-reduced-motion: reduce, including live preference changes.
These facts come from static analysis of the production bundle (the original site was not executed):
| Original site | This demo |
|---|---|
CursorTrailAnimation component, lazy-loaded via next/dynamic (chunk 2834 + CSS 7583) |
createCursorTrail() in trail.js |
trailAnimationArea / trailAnimationIgnore class names |
same names |
20px grid, Math.floor(clientX / 20) * 20 |
grid: 20 by default, adjustable in the panel |
| 10 fixed-position blocks in rotation | pool: 10 by default |
6 brand colors #ae85ff #f73678 #c05200 #583af7 #5fed83 #bba00a |
identical |
11 glyphs - / ) . + = > < * & ( |
identical |
| random lifetime of 100–400ms | minLife: 100 / maxLife: 400 |
.Color scales 0 → 1 in 0.1s |
same curve and duration |
desktop mousemove, ignoring text/links/buttons |
same (links and buttons use closest) |
Include trail.js and the .trail-layer / .trail-block rules from styles.css, then:
<section class="trailAnimationArea">
<h1 class="trailAnimationIgnore">The heading does not trigger</h1>
<p>This empty background will grow blocks.</p>
</section>
<script src="trail.js"></script>
<script>
const trail = createCursorTrail({ grid: 20, pool: 10 });
</script>| Option | Default | Description |
|---|---|---|
grid |
20 |
Grid size in px; also the block size |
pool |
10 |
Number of blocks in the pool |
minLife / maxLife |
100 / 400 |
Lifetime range per block (ms) |
blankChance |
0.3 |
Chance a block shows no glyph |
colors |
6 brand colors | Color pool |
glyphs |
11 ASCII characters | Glyph pool |
areaSelector |
.trailAnimationArea |
Areas where blocks may appear |
ignoreSelector |
.trailAnimationIgnore |
Subtrees to exclude |
onChange |
null |
State callback receiving { spawned, active, reduced, paused } |
Returns a controller:
| Method | Description |
|---|---|
destroy() |
Remove listeners, timers and DOM |
setPaused(value) |
Pause or resume the trail |
isReduced() |
Whether it is currently disabled by reduced motion |
getState() |
Current counters |
- The original is a React component; this demo is plain JS and renders blocks in a fixed layer instead of the React tree.
- The original checks only whether the event target's
tagNameisA/BUTTON; this demo usesclosest, so children inside links do not trigger either. - Glyphs are centered inside the block here, and the demo adds a parameter panel, counters and a grid overlay.
- The original also listens on
Riverheading elements; this demo does not. AddtrailAnimationAreato a heading if you need that.
Tested with headless Chrome + CDP:
- moving over empty areas spawns blocks, and the counters match the number of
is-activenodes; trailAnimationIgnoreregions (headings, body text) do not trigger, and leaving them resumes the trail;- the pause toggle works, and resuming continues the trail;
- emulating
prefers-reduced-motion: reducehides the layer, removes all listeners and shows the page notice; - changing the grid or pool size updates the block count and
--trail-sizetogether.
preview.png is the trail captured during that run.
- Blocks are
position: fixedand positioned in viewport coordinates, so they stay aligned to the 20px grid while scrolling. - Blocks linger briefly after leaving their area until their lifetime ends; that is original behavior, not a bug.
- Mouse events inside an
iframedo not bubble out, so content embedded in an iframe will not spawn blocks above it.
