Dot Pattern
BackgroundsA field of dots behind content, faded toward the edges, with an optional slow pulse.
A perspective grid floor sliding toward the viewer. Two repeating gradients on one plane rotated in 3D; the motion is a translate of exactly one cell on the compositor, so the loop has no visible seam and never repaints. A mask fades the horizon. Zero JS; fills its positioned parent.
Ship it.
one cell per second · compositor
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.1 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 120 · Firefox 53 · Safari 15.4; needs mask-image. Elsewhere: the grid without the horizon fade.
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 RetroGrid from '@moonarc/core/RetroGrid';
---
<section class="relative overflow-hidden">
<RetroGrid />
<h1 class="relative">Above the floor</h1>
</section>Owns the file, no dependency. The registry item also installs the base tokens.
npx shadcn@latest add https://moonarc.dev/r/retro-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.
---
/**
* RetroGrid — a perspective grid floor sliding toward the viewer. Two
* repeating gradients on one plane rotated in 3D; the motion is a
* `translate` of exactly one cell on the compositor (not background-position,
* which repaints every frame), so the loop is seamless and costs nothing
* on the main thread. A mask fades the horizon. Fills its positioned parent.
*/
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'div'> {
/** Tilt of the floor in degrees; 65 is a floor, 45 a ramp. */
angle?: number;
/** Cell size in px. */
cellSize?: number;
/** Line colour. */
color?: string;
/** Seconds per cell of travel. */
speed?: number;
/** Fade out toward the horizon. */
fade?: boolean;
/** Line width in px. */
lineWidth?: number;
/** Where the horizon sits, as a percentage of the height. */
horizon?: number;
}
const { angle = 65, cellSize = 60, color = 'color-mix(in srgb, var(--ma-ink) 22%, transparent)', speed = 1, fade = true, lineWidth = 1, horizon = 40, class: className, style, ...rest } = Astro.props;
const inline = [`--ma-retro-angle:${angle}deg`, `--ma-retro-cell:${cellSize}px`, `--ma-retro-color:${color}`, `--ma-retro-dur:${speed}s`, `--ma-retro-line:${lineWidth}px`, `--ma-retro-horizon:${horizon}%`, typeof style === 'string' ? style : '']
.filter(Boolean)
.join(';');
---
<div class:list={['ma-retro', className]} aria-hidden="true" data-fade={fade ? '' : undefined} style={inline} {...rest}>
<div class="ma-retro__floor"><div class="ma-retro__lines"></div></div>
</div>
<style is:global>
@layer components {
:where(.ma-retro) {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
perspective: calc(var(--ma-retro-cell, 60px) * 4);
contain: strict;
}
:where(.ma-retro[data-fade]) {
mask-image: linear-gradient(to bottom, transparent var(--ma-retro-horizon, 40%), #000 calc(var(--ma-retro-horizon, 40%) + 30%));
}
/* the floor: a huge plane hinged at the horizon, rotated back */
:where(.ma-retro__floor) {
position: absolute;
inset: var(--ma-retro-horizon, 40%) -100% -100%;
transform-origin: 50% 0;
transform: rotateX(var(--ma-retro-angle, 65deg));
}
:where(.ma-retro__lines) {
position: absolute;
inset: calc(-1 * var(--ma-retro-cell, 60px)) 0 0;
background-image:
linear-gradient(to right, var(--ma-retro-color) var(--ma-retro-line, 1px), transparent var(--ma-retro-line, 1px)),
linear-gradient(to bottom, var(--ma-retro-color) var(--ma-retro-line, 1px), transparent var(--ma-retro-line, 1px));
background-size: var(--ma-retro-cell, 60px) var(--ma-retro-cell, 60px);
background-position: 50% 0;
will-change: translate;
animation: ma-retro var(--ma-retro-dur) linear infinite;
}
@media (prefers-reduced-motion: reduce) {
:where(.ma-retro__lines) {
animation: none;
}
}
}
@keyframes ma-retro {
from {
translate: 0 0;
}
to {
translate: 0 var(--ma-retro-cell, 60px);
}
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
angle | number | 65 | Tilt of the floor in degrees; 65 reads as a floor, 45 as a ramp. |
cellSize | number | 60 | Cell size in px; the perspective distance is four cells. |
color | string | ink at 22 % | Line colour. |
speed | number | 1 | Seconds per cell of travel. |
fade | boolean | true | Fade out toward the horizon. |
lineWidth | number | 1 | Line width in px. |
horizon | number | 40 | Where the horizon sits, as a percentage of the height. |
The floor stands still.
ClientRouterCSS-only; nothing to rebind.
Replaces: RetroGrid (Magic UI) · GridBackground (Aceternity). See the migration table.
A field of dots behind content, faded toward the edges, with an optional slow pulse.
A hairline grid behind content, faded toward the edges, optionally drifting one cell at a time.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown