Skip to content

Components / Backgrounds

Noise

Film grain over a gradient or image, blended and flickering at 12 fps. One SVG turbulence tile as a background whose translate steps; pure CSS, no canvas. Fills its positioned parent.

Live demo

Grain.

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 4.9 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

every browser.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import Noise from '@moonarc/core/Noise';
---
<section class="relative">
  <Aurora />
  <Noise opacity={0.1} />
  <h2 class="relative">Grain over aurora</h2>
</section>
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/noise.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win; it imports nothing.

Noise.astro
---
/**
 * Noise — film grain over whatever is beneath. Zero JS: one SVG turbulence
 * tile as a background, blended, with its translate stepped at 12 fps so
 * the grain lives without repainting. Fills its positioned parent and clips
 * the moving grain to it, so it never widens the page.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Strength, 0–1. */
  opacity?: number;
  /** Flicker the grain. */
  animate?: boolean;
  /** Blend mode against the layers beneath. */
  blend?: 'overlay' | 'soft-light' | 'multiply' | 'screen' | 'normal';
  /** Grain size: tile size in px. Smaller is finer. */
  size?: number;
}

const { opacity = 0.08, animate = true, blend = 'overlay', size = 200, class: className, style, ...rest } = Astro.props;
const vars = [`--ma-noise-o:${opacity}`, `--ma-noise-blend:${blend}`, `--ma-noise-size:${size}px`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<div class:list={['ma-noise', className]} aria-hidden="true" data-animate={animate ? '' : undefined} style={vars} {...rest}></div>

<style is:global>
  @layer components {
    /* the box is the parent's size and clips; the grain is a pseudo-element 10% larger on every side, so its translate
       never shows an edge. The oversized box itself used to be the grain, and it widened any page it touched the edge of */
    :where(.ma-noise) {
      position: absolute;
      inset: 0;
      overflow: hidden;
      pointer-events: none;
      mix-blend-mode: var(--ma-noise-blend, overlay);
      opacity: var(--ma-noise-o, 0.08);
    }
    :where(.ma-noise)::before {
      content: '';
      position: absolute;
      inset: -10%;
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 0 0.5 0 0 0 1 0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
      background-size: var(--ma-noise-size, 200px) var(--ma-noise-size, 200px);
    }
    :where(.ma-noise[data-animate])::before {
      animation: ma-noise 0.5s steps(6) infinite;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-noise)::before {
        animation: none;
      }
    }
  }

  @keyframes ma-noise {
    to {
      translate: 5% 3%;
    }
  }
</style>

Props

PropTypeDefaultDescription
opacitynumber0.08Strength, 0–1. 0.04–0.12 reads as texture; more reads as static.
animatebooleantrueFlicker the grain by stepping its position at 12 fps.
blend'overlay' | 'soft-light' | 'multiply' | 'screen' | 'normal''overlay'Blend mode against the layers beneath.
sizenumber200Tile size in px; smaller is finer grain.

Reduced motion

The grain holds still.

With ClientRouter

CSS-only; nothing to rebind.

Why it is built this way

Replaces: Noise (React Bits) · grainy gradient canvases. 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.

every browseralways

Dot Pattern

Backgrounds

A field of dots behind content, faded toward the edges, with an optional slow pulse.

every browseralways

Grid Pattern

Backgrounds

A hairline grid behind content, faded toward the edges, optionally drifting one cell at a time.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown