# Dot Pattern (Moonarc)

A field of dots behind content, faded toward the edges, with an optional slow pulse. One tiled radial gradient and a mask; pure CSS. Fills its positioned parent.

- Import: `import DotPattern from '@moonarc/core/DotPattern'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/dot-pattern.json`
- Tier A · category background · trigger always
- Readout: `<DotPattern size={20}>`
- Browser support: widely (Chrome 120 · Firefox 53 · Safari 15.4)
- Measured cost: 0 B JS (CSS 4.6 kB raw)
- Page: https://moonarc.dev/components/dot-pattern/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | `number` | `20` | Distance between dots in px. |
| `radius` | `number` | `1` | Dot radius in px. |
| `color` | `string` | `var(--ma-glow)` | Dot colour; the token is theme-aware. |
| `fade` | `boolean` | `true` | Radial mask toward the edges. |
| `pulse` | `number` | `0` | Seconds per pulse (opacity 0.5 ↔ 1); 0 keeps it still. |

## Usage

```astro
<section class="relative">
  <DotPattern pulse={5} />
  <h2 class="relative">Above the dots</h2>
</section>
```

## Reduced motion

No pulse.

## With ClientRouter

CSS-only; nothing to rebind.

## Craft

- The pulse is opacity on the whole layer (compositor only); the gradient itself never changes.
- Dot edge is radius + 0.5 px so it stays crisp at any device pixel ratio.

## Replaces

- DotPattern (Magic UI)
- DotBackground (Aceternity)

## Source

```astro
---
/**
 * DotPattern — a field of dots behind content, faded at the edges, with an
 * optional slow pulse. Zero JS: one radial gradient tiled, and a mask.
 * Fills its positioned parent.
 */
import type { HTMLAttributes } from 'astro/types';

interface Props extends HTMLAttributes<'div'> {
  /** Distance between dots in px. */
  size?: number;
  /** Dot radius in px. */
  radius?: number;
  /** Dot colour. */
  color?: string;
  /** Fade toward the edges. */
  fade?: boolean;
  /** Pulse period in seconds; 0 keeps it still. */
  pulse?: number;
}

const { size = 20, radius = 1, color = 'var(--ma-glow)', fade = true, pulse = 0, class: className, style, ...rest } = Astro.props;
const vars = [`--ma-dp-size:${size}px`, `--ma-dp-r:${radius}px`, `--ma-dp-color:${color}`, `--ma-dp-pulse:${pulse}s`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

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

<style is:global>
  @layer components {
    :where(.ma-dot-pattern) {
      position: absolute;
      inset: 0;
      pointer-events: none;
      background-image: radial-gradient(circle, var(--ma-dp-color, var(--ma-glow)) var(--ma-dp-r, 1px), transparent calc(var(--ma-dp-r, 1px) + 0.5px));
      background-size: var(--ma-dp-size, 20px) var(--ma-dp-size, 20px);
    }
    :where(.ma-dot-pattern[data-fade]) {
      mask-image: radial-gradient(ellipse at center, #000 30%, transparent 75%);
    }
    :where(.ma-dot-pattern[data-pulse]) {
      animation: ma-dot-pattern var(--ma-dp-pulse, 4s) var(--ma-ease-in-out) infinite alternate;
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-dot-pattern) {
        animation: none;
      }
    }
  }

  @keyframes ma-dot-pattern {
    from {
      opacity: 0.5;
    }
    to {
      opacity: 1;
    }
  }
</style>

```
