# Noise (Moonarc)

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.

- Import: `import Noise from '@moonarc/core/Noise'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/noise.json`
- Tier A · category background · trigger always
- Readout: `<Noise opacity={0.08}>`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 4.9 kB raw)
- Page: https://moonarc.dev/components/noise/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `opacity` | `number` | `0.08` | Strength, 0–1. 0.04–0.12 reads as texture; more reads as static. |
| `animate` | `boolean` | `true` | Flicker the grain by stepping its position at 12 fps. |
| `blend` | `'overlay' | 'soft-light' | 'multiply' | 'screen' | 'normal'` | `'overlay'` | Blend mode against the layers beneath. |
| `size` | `number` | `200` | Tile size in px; smaller is finer grain. |

## Usage

```astro
<section class="relative">
  <Aurora />
  <Noise opacity={0.1} />
  <h2 class="relative">Grain over aurora</h2>
</section>
```

## Reduced motion

The grain holds still.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- steps(6) over 500 ms: twelve grain frames a second, the cadence of film, and every frame is a translate, with no feTurbulence re-render.
- The tile is stitched, so the seam never shows at any translate.
- The grain is a layer 10% larger than the box on every side, clipped by the box: the translate never shows an edge, and the grain never reaches past the parent to widen the page.
- overlay blend adds texture without shifting the colours beneath; soft-light is gentler, multiply darkens.

## Replaces

- Noise (React Bits)
- grainy gradient canvases

## Source

```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>

```
