Aurora Background
BackgroundsAmbient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.
A field of cells that light up under the cursor and fade where it has been, with a slow idle sweep when nothing is touched. Zero JavaScript: every cell is an element whose own hover state decays on a transition, so the trail is the browser doing the work. Fills its positioned parent.
move your cursor · 0 B
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript of its own
0 B
CSS 5.4 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 105 · Firefox 121 · Safari 15.4; needs has.
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
import CursorGrid from '@moonarc/core/CursorGrid';
---
<section class="relative min-h-[60vh]">
<CursorGrid color="oklch(78% 0.16 60)" />
<h1 class="relative">Move your cursor. Zero bytes of JavaScript.</h1>
</section>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/cursor-grid.jsonThe CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.
The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.
---
/**
* CursorGrid — a field of cells that light where the cursor is and fade where
* it was. Zero JS: every cell is an element with its own :hover, lit
* instantly and released on a slow transition, so the trail is the browser's
* own hover state decaying. An idle sweep keeps the field alive when nothing
* touches it. Fills its positioned parent; put content above it.
*
* The cost is in the HTML, not the script: cols × rows empty elements
* (32 × 16 = 512 by default, ~5 kB of markup, ~120 B gzipped).
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'div'> {
/** Columns. */
cols?: number;
/** Rows. */
rows?: number;
/** Colour of a lit cell; any CSS colour. */
color?: string;
/** How long a cell takes to go dark after the cursor leaves it, ms. */
fade?: number;
/** Slow idle sweep across the field when nothing is hovered. */
idle?: boolean;
/** Show the hairline grid between cells. */
lines?: boolean;
}
const { cols = 32, rows = 16, color = 'currentColor', fade = 900, idle = true, lines = true, class: className, style, ...rest } = Astro.props;
const count = Math.max(1, Math.min(cols, 96)) * Math.max(1, Math.min(rows, 64));
const vars = [`--ma-cg-cols:${cols}`, `--ma-cg-rows:${rows}`, `--ma-cg-color:${color}`, `--ma-cg-fade:${fade}ms`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---
<div
class:list={['ma-cursor-grid', className]}
aria-hidden="true"
data-idle={idle ? '' : undefined}
data-lines={lines ? '' : undefined}
style={vars}
{...rest}
>
{Array.from({ length: count }, () => <i></i>)}
</div>
<style is:global>
@layer components {
:where(.ma-cursor-grid) {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(var(--ma-cg-cols, 32), 1fr);
grid-template-rows: repeat(var(--ma-cg-rows, 16), 1fr);
overflow: hidden;
contain: strict;
}
:where(.ma-cursor-grid[data-lines]) {
background-image:
linear-gradient(to right, var(--ma-edge) 1px, transparent 1px),
linear-gradient(to bottom, var(--ma-edge) 1px, transparent 1px);
background-size: calc(100% / var(--ma-cg-cols, 32)) calc(100% / var(--ma-cg-rows, 16));
}
:where(.ma-cursor-grid > i) {
display: block;
background: var(--ma-cg-color, currentColor);
opacity: 0;
transition: opacity var(--ma-cg-fade, 900ms) var(--ma-ease-out);
}
/* lit at once, released slowly — the trail is the hover state decaying */
:where(.ma-cursor-grid > i:hover) {
opacity: 0.9;
transition-duration: 0ms;
}
:where(.ma-cursor-grid > i:hover + i),
:where(.ma-cursor-grid > i:has(+ i:hover)) {
opacity: 0.35;
transition-duration: 60ms;
}
/* idle sweep: a soft band drifting across the field, paused while the cursor is in it */
:where(.ma-cursor-grid[data-idle])::after {
content: '';
position: absolute;
inset: -20% -60%;
background: linear-gradient(100deg, transparent 40%, var(--ma-cg-color, currentColor) 50%, transparent 60%);
opacity: 0.12;
will-change: translate;
animation: ma-cursor-grid-sweep 9s var(--ma-ease-in-out) infinite alternate;
pointer-events: none;
}
:where(.ma-cursor-grid[data-idle]:hover)::after {
animation-play-state: paused;
opacity: 0;
transition: opacity 600ms;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-cursor-grid[data-idle])::after {
animation: none;
opacity: 0;
}
:where(.ma-cursor-grid > i) {
transition-duration: 200ms;
}
}
}
@keyframes ma-cursor-grid-sweep {
from {
translate: -35% 0;
}
to {
translate: 35% 0;
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
cols | number | 32 | Columns, max 96. Cells are cols × rows empty elements: the cost is HTML, about 10 B raw a cell. |
rows | number | 16 | Rows, max 64. |
color | string | 'currentColor' | Colour of a lit cell. |
fade | number | 900 | Milliseconds a cell takes to go dark after the cursor leaves. Longer means a longer trail. |
idle | boolean | true | A soft band drifts across the field when nothing is hovered; pauses on hover. |
lines | boolean | true | Draw the hairline grid between cells. |
The idle sweep stops; cells still light under the cursor but release in 200 ms instead of trailing.
ClientRouterCSS-only; nothing to rebind.
Replaces: InteractiveGridPattern (Magic UI) · DotGrid (React Bits). See the migration table.
Ambient gradient field: three soft colour fields drifting at three coprime speeds, a slow hue rotation so the colours themselves move, and a film-grain layer stepped at 12 fps.
A field of braille dots that reads as a flowing contour map: two waves cross it, the colour turns along a sunset arc, and the cursor carves a trail that decays one dot at a time.
A hairline grid behind content, faded toward the edges, optionally drifting one cell at a time.
A soft radial light follows the cursor over a card and fades in only while hovered.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown