# Background Shapes

> Generative glyph grid background — two mirrored SVG panels of dots, crosses, stripes and squares, each cell swapping to a random shape on its own timer.

A shadcn/ui block from the [@uicapsule](https://uicapsule.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/background-shapes).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/background-shapes
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uicapsule/background-shapes-light-34972508a07466a8.png
- Registry item: https://uicapsule.com/r/background-shapes.json
- Categories: effects, geometric
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uicapsule.com/r/background-shapes.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `/background-shapes.tsx`

```tsx
"use client";

import { useEffect, useMemo, useState } from "react";
import type { ReactElement, ReactNode } from "react";

interface CellProps {
  colors: string[];
  strokeWidth: number;
}

type CellComponent = (props: CellProps) => ReactElement | null;

// Cell glyph functions that return JSX instead of SVG strings
const Cell1 = ({ colors }: CellProps) => (
  <circle cx="50" cy="50" r="9.44" fill={colors[0]} fillRule="evenodd" />
);

const Cell2 = ({ colors, strokeWidth }: CellProps) => (
  <>
    <line x1="25" x2="75" y1="25" y2="25" stroke={colors[0]} strokeWidth={strokeWidth} />
    <line x1="25" x2="75" y1="50" y2="50" stroke={colors[0]} strokeWidth={strokeWidth} />
    <line x1="25" x2="75" y1="75" y2="75" stroke={colors[0]} strokeWidth={strokeWidth} />
  </>
);

const Cell3 = ({ colors, strokeWidth }: CellProps) => (
  <>
    <line x1="25" x2="75" y1="25" y2="75" stroke={colors[0]} strokeWidth={strokeWidth} />
    <line x1="25" x2="75" y1="75" y2="25" stroke={colors[0]} strokeWidth={strokeWidth} />
  </>
);

const Cell4 = ({ colors, strokeWidth }: CellProps) => (
  <rect
    width="50"
    height="50"
    x="25"
    y="25"
    fill="none"
    stroke={colors[0]}
    strokeWidth={strokeWidth}
  />
);

const Cell5 = ({ colors, strokeWidth }: CellProps) => (
  <line x1="25" x2="75" y1="75" y2="25" fill="none" stroke={colors[0]} strokeWidth={strokeWidth} />
);

const Cell6 = () => null;

const Cell7 = () => <rect width="75" height="75" x="12.5" y="12.5" fill="rgba(255,255,255,0.1)" />;

interface CellConfig {
  glyph: CellComponent;
  weight: number;
}

const cellConfigs: CellConfig[] = [
  { glyph: Cell1, weight: 1 },
  { glyph: Cell2, weight: 1 },
  { glyph: Cell3, weight: 1 },
  { glyph: Cell4, weight: 1 },
  { glyph: Cell5, weight: 1 },
  { glyph: Cell6, weight: 5 },
  { glyph: Cell7, weight: 3 },
];

// Each config repeated `weight` times, so a uniform draw honours the weights.
// Built once at module scope: every cell re-rolls on its own timer, so rebuilding
// this per draw would allocate constantly.
const weightedCells: CellConfig[] = cellConfigs.flatMap((config) =>
  Array.from({ length: config.weight }, () => config),
);

// Unreachable fallback for `noUncheckedIndexedAccess`; the index below is always in range.
const fallbackCell: CellConfig = { glyph: Cell1, weight: 1 };

const pickCell = (): CellConfig =>
  weightedCells[Math.floor(Math.random() * weightedCells.length)] ?? fallbackCell;

// Cells are authored in a 100x100 viewBox; this maps one down to a `cellSize` grid slot.
const CELL_SCALE = 0.2;

// Module scope so the default keeps a stable identity across renders.
const DEFAULT_COLORS = ["white"];

// Individual cell component that manages its own interval
interface GridCellProps {
  x: number;
  y: number;
  colors: string[];
  strokeWidth: number;
  minInterval: number;
  maxInterval: number;
}

const GridCell = ({ x, y, colors, strokeWidth, minInterval, maxInterval }: GridCellProps) => {
  const [currentCell, setCurrentCell] = useState<CellConfig>(pickCell);

  useEffect(() => {
    const getRandomInterval = () => Math.random() * (maxInterval - minInterval) + minInterval;

    let timeoutId: ReturnType<typeof setTimeout>;
    const scheduleNext = () => {
      timeoutId = setTimeout(() => {
        setCurrentCell(pickCell());
        scheduleNext();
      }, getRandomInterval());
    };
    scheduleNext();

    return () => clearTimeout(timeoutId);
  }, [minInterval, maxInterval]);

  const Glyph = currentCell.glyph;

  return (
    <g transform={`translate(${x} ${y})`}>
      <g transform={`scale(${CELL_SCALE})`}>
        <Glyph colors={colors} strokeWidth={strokeWidth} />
      </g>
    </g>
  );
};

interface BackgroundGlyphsProps {
  width?: number;
  height?: number;
  cellSize?: number;
  strokeWidth?: number;
  colors?: string[];
  className?: string;
  minInterval?: number;
  maxInterval?: number;
}

export const BackgroundGlyphs = ({
  width = 500,
  height = 500,
  cellSize = 20,
  strokeWidth = 10,
  colors = DEFAULT_COLORS,
  className = "",
  minInterval = 1000,
  maxInterval = 5000,
}: BackgroundGlyphsProps) => {
  const borderSize = cellSize * 2;

  const cells = useMemo<ReactNode[]>(() => {
    const list: ReactNode[] = [];
    for (let x = borderSize; x < width / 2; x += cellSize) {
      for (let y = borderSize; y < height - borderSize; y += cellSize) {
        list.push(
          <GridCell
            key={`left-${x}-${y}`}
            x={x}
            y={y}
            colors={colors}
            strokeWidth={strokeWidth}
            minInterval={minInterval}
            maxInterval={maxInterval}
          />,
          <GridCell
            key={`right-${x}-${y}`}
            x={width - cellSize - x}
            y={y}
            colors={colors}
            strokeWidth={strokeWidth}
            minInterval={minInterval}
            maxInterval={maxInterval}
          />,
        );
      }
    }
    return list;
  }, [width, height, cellSize, strokeWidth, colors, borderSize, minInterval, maxInterval]);

  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={className}>
      {cells}
    </svg>
  );
};
```

### `/preview.tsx`

```tsx
"use client";

import { useEffect, useState } from "react";

import { BackgroundGlyphs } from "./background-shapes";

const Preview = () => {
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);

  useEffect(() => {
    const update = () => setSize({ height: window.innerHeight, width: window.innerWidth });
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);

  return (
    <div className="h-full w-full bg-[#2164D6]">
      {size && <BackgroundGlyphs width={size.width} height={size.height} />}
    </div>
  );
};

export default Preview;
```
