Skip to content
UI Kit
Accent color

Pattern

Background patterns in the theme colors. Dots, grids, lines, contour lines, grain, light fields, waves, halftone and rings, each with tone, fade, seed and a very slow drift.

One of 3 components in Patterns and shapes

8 variants

Variants

Dots

A dot field that drifts by one cell per loop. The hero shows tone and fade live.

Grid

Fine lines and a stronger line every fourth cell, with a small mark on each crossing.

Lines

Ruled lines and diagonal hatching for editorial pages and quiet bands.

Contour lines

Topographic lines from a seeded height field, traced with marching squares and smoothed. Every fourth line is an index line that glows slowly.

Grain

An SVG turbulence grain, colored by a token. Over a mesh it reads like printed colour.

Light fields and mesh

Soft light fields that drift on their own slow paths, or a full mesh that turns a few degrees. Both mix from the accent.

Waves

Wave lines that drift sideways by exactly one wave per loop.

Halftone

Soft dots weighted by a light field and cut at half strength, so they grow toward the light. The light wanders slowly.

Details

What Pattern brings along, read from the registry and the source.

Variants
8
Version
1.0.0 · since 1.0.0
File
components/ui/pattern.tsx
Brings along
utilskitmotion-scene
npm packages
None
Languages
Texts come through props
Studio
Adjust in code

Props

The PatternProps interface from components/ui/pattern.tsx, with the comments from the code.

  • kindoptional
    PatternKind

    dotsgridlinesdiagonalcrosswavescontoursnoisegradientmeshhalftonerings

    What is drawn. Default dots.

  • sizeoptional
    number

    Scale in pixels: the tile for dots, grid, lines, diagonal, cross and waves, the line spacing for contours and rings, the dot pitch for halftone and the grain for noise. Each kind has its own default.

  • toneoptional
    PatternTone

    mutedaccent

    muted (default) draws in the foreground at low strength, accent in the accent color

  • fadeoptional
    PatternFade

    noneradialtopbottomedges

    Where the pattern fades out. top and bottom name the edge that fades. Default none.

  • seedoptional
    number

    Picks positions and shapes for contours, halftone, rings, gradient and mesh

  • animateoptional
    boolean

    A very slow drift or shimmer, paused out of view, off with reduced motion

  • strengthoptional
    number

    0 to 1, how strong the lines are. Default 1.

  • styleoptional
    React.CSSProperties

Accessibility

Read from the source of the component.

  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • Brings animations of its own, calmly timed and without wobble.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 431 lines
components/ui/pattern.tsx
'use client';

import { useMemo } from 'react';
import { cn, round } from '@/lib/kit';
import { SCENE_CSS, rand, useScenePlay, useSceneId } from '@/components/ui/motion-scene';

/*
 * Background patterns, drawn in SVG and CSS with the kit tokens. They fill
 * their parent (absolute inset-0) and never take clicks. Every pattern is
 * deterministic: the same kind, size and seed give the same picture on the
 * server and in the browser.
 *
 * The optional drift is very slow, moves only transform or opacity, pauses
 * out of view and stays off with reduced motion. Tiled patterns move by
 * exactly one tile per loop, so the loop has no seam.
 */

export type PatternKind = 'dots' | 'grid' | 'lines' | 'diagonal' | 'cross' | 'waves' | 'contours' | 'noise' | 'gradient' | 'mesh' | 'halftone' | 'rings';
export type PatternTone = 'muted' | 'accent';
export type PatternFade = 'none' | 'radial' | 'top' | 'bottom' | 'edges';

export interface PatternProps {
  /** What is drawn. Default dots. */
  kind?: PatternKind;
  /**
   * Scale in pixels: the tile for dots, grid, lines, diagonal, cross and waves,
   * the line spacing for contours and rings, the dot pitch for halftone and the
   * grain for noise. Each kind has its own default.
   */
  size?: number;
  /** muted (default) draws in the foreground at low strength, accent in the accent color */
  tone?: PatternTone;
  /** Where the pattern fades out. top and bottom name the edge that fades. Default none. */
  fade?: PatternFade;
  /** Picks positions and shapes for contours, halftone, rings, gradient and mesh */
  seed?: number;
  /** A very slow drift or shimmer, paused out of view, off with reduced motion */
  animate?: boolean;
  /** 0 to 1, how strong the lines are. Default 1. */
  strength?: number;
  className?: string;
  style?: React.CSSProperties;
}

const SIZE: Record<PatternKind, number> = { dots: 22, grid: 32, lines: 14, diagonal: 12, cross: 28, waves: 16, contours: 24, noise: 2, gradient: 1, mesh: 1, halftone: 14, rings: 34 };

const FADE: Record<PatternFade, React.CSSProperties> = {
  none: {},
  radial: { maskImage: 'radial-gradient(ellipse 72% 70% at 50% 50%, #000 22%, transparent 80%)' },
  top: { maskImage: 'linear-gradient(to top, #000 30%, transparent 96%)' },
  bottom: { maskImage: 'linear-gradient(to bottom, #000 30%, transparent 96%)' },
  edges: {
    maskImage: 'linear-gradient(to right, transparent, #000 16%, #000 84%, transparent), linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent)',
    maskComposite: 'intersect',
    WebkitMaskComposite: 'source-in',
  } as React.CSSProperties,
};

/** The three ink strengths and the solid ink, as CSS variables on the root */
function inks(tone: PatternTone, strength: number): React.CSSProperties {
  const s = Math.max(0, Math.min(1, strength));
  const base = tone === 'accent' ? 'var(--primary)' : 'var(--foreground)';
  const levels = tone === 'accent' ? [24, 42, 74] : [12, 20, 34];
  return {
    '--pt-1': `color-mix(in oklab, ${base} ${round(levels[0] * s, 1)}%, transparent)`,
    '--pt-2': `color-mix(in oklab, ${base} ${round(levels[1] * s, 1)}%, transparent)`,
    '--pt-3': `color-mix(in oklab, ${base} ${round(levels[2] * s, 1)}%, transparent)`,
    '--pt-solid': base,
  } as React.CSSProperties;
}

/* ------------------------------------------------------------------ */
/* Contour lines: marching squares over a seeded height field          */
/* ------------------------------------------------------------------ */

const CW = 1200;
const CH = 800;

function contourPaths(seed: number, levels: number) {
  const NX = 48;
  const NY = 32;
  const sx = CW / NX;
  const sy = CH / NY;
  const hills = Array.from({ length: 7 }, (_, i) => ({
    x: rand(seed, i) * CW,
    y: rand(seed, i + 11) * CH,
    s: 140 + rand(seed, i + 23) * 260,
    h: (rand(seed, i + 37) > 0.28 ? 1 : -0.7) * (0.45 + rand(seed, i + 41) * 0.8),
  }));
  const ph = rand(seed, 53) * 6.28;
  const field = (x: number, y: number) => {
    let v = 0.18 * Math.sin(x / 260 + ph) * Math.cos(y / 210 - ph);
    for (const k of hills) {
      const dx = x - k.x;
      const dy = y - k.y;
      v += k.h * Math.exp(-(dx * dx + dy * dy) / (2 * k.s * k.s));
    }
    return v;
  };
  const v: number[][] = [];
  let min = Infinity;
  let max = -Infinity;
  for (let j = 0; j <= NY; j++) {
    v[j] = [];
    for (let i = 0; i <= NX; i++) {
      const f = field(i * sx, j * sy);
      v[j][i] = f;
      if (f < min) min = f;
      if (f > max) max = f;
    }
  }
  const paths: { d: string; major: boolean }[] = [];
  for (let l = 1; l <= levels; l++) {
    const t = min + ((max - min) * l) / (levels + 1);
    const pts = new Map<string, [number, number]>();
    const links = new Map<string, string[]>();
    const point = (key: string, p: [number, number]) => {
      if (!pts.has(key)) pts.set(key, p);
      return key;
    };
    const lerp = (a: number, b: number) => (t - a) / (b - a);
    // edge points: h(i,j) on the top edge of row j between i and i+1, v(i,j) on the left edge of column i between j and j+1
    const top = (i: number, j: number) => point(`h${i},${j}`, [(i + lerp(v[j][i], v[j][i + 1])) * sx, j * sy]);
    const left = (i: number, j: number) => point(`v${i},${j}`, [i * sx, (j + lerp(v[j][i], v[j + 1][i])) * sy]);
    const link = (a: string, b: string) => {
      links.set(a, [...(links.get(a) ?? []), b]);
      links.set(b, [...(links.get(b) ?? []), a]);
    };
    for (let j = 0; j < NY; j++) {
      for (let i = 0; i < NX; i++) {
        const a = v[j][i] > t ? 1 : 0;
        const b = v[j][i + 1] > t ? 1 : 0;
        const c = v[j + 1][i + 1] > t ? 1 : 0;
        const d = v[j + 1][i] > t ? 1 : 0;
        const code = a * 8 + b * 4 + c * 2 + d;
        if (code === 0 || code === 15) continue;
        const T = () => top(i, j);
        const R = () => left(i + 1, j);
        const B = () => top(i, j + 1);
        const L = () => left(i, j);
        const centre = (v[j][i] + v[j][i + 1] + v[j + 1][i + 1] + v[j + 1][i]) / 4 > t;
        switch (code) {
          case 1: case 14: link(L(), B()); break;
          case 2: case 13: link(B(), R()); break;
          case 3: case 12: link(L(), R()); break;
          case 4: case 11: link(T(), R()); break;
          case 6: case 9: link(T(), B()); break;
          case 7: case 8: link(L(), T()); break;
          case 5: if (centre) { link(L(), T()); link(B(), R()); } else { link(L(), B()); link(T(), R()); } break;
          case 10: if (centre) { link(T(), R()); link(L(), B()); } else { link(L(), T()); link(B(), R()); } break;
        }
      }
    }
    // chain the segments into lines, open ends first
    const used = new Set<string>();
    const chains: [number, number][][] = [];
    const walk = (start: string) => {
      const chain: string[] = [start];
      used.add(start);
      let cur = start;
      for (;;) {
        const next = (links.get(cur) ?? []).find((n) => !used.has(n));
        if (!next) break;
        used.add(next);
        chain.push(next);
        cur = next;
      }
      const closed = chain.length > 2 && (links.get(cur) ?? []).includes(start);
      const out = chain.map((k) => pts.get(k)!);
      if (closed) out.push(out[0]);
      if (out.length > 3) chains.push(out);
    };
    for (const [k, n] of links) if (n.length === 1 && !used.has(k)) walk(k);
    for (const k of links.keys()) if (!used.has(k)) walk(k);
    // smooth through the midpoints, the vertices are the control points
    let d = '';
    for (const c of chains) {
      const m = (p: [number, number], q: [number, number]) => `${Math.round((p[0] + q[0]) / 2)} ${Math.round((p[1] + q[1]) / 2)}`;
      d += `M${Math.round(c[0][0])} ${Math.round(c[0][1])}L${m(c[0], c[1])}`;
      for (let k = 1; k < c.length - 1; k++) d += `Q${Math.round(c[k][0])} ${Math.round(c[k][1])} ${m(c[k], c[k + 1])}`;
      d += `L${Math.round(c[c.length - 1][0])} ${Math.round(c[c.length - 1][1])}`;
    }
    if (d) paths.push({ d, major: l % 4 === 0 });
  }
  return paths;
}

/* ------------------------------------------------------------------ */
/* Grain                                                               */
/* ------------------------------------------------------------------ */

const grain = (freq: number) =>
  `url("data:image/svg+xml;utf8,${encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='g' x='0' y='0'><feTurbulence type='fractalNoise' baseFrequency='${freq}' numOctaves='2' stitchTiles='stitch'/><feColorMatrix type='matrix' values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 2.4 -1.1'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>`,
  ).replace(/%2523/g, '%23')}")`;

/* ------------------------------------------------------------------ */
/* The component                                                       */
/* ------------------------------------------------------------------ */

/**
 * A background pattern that fills its parent. Put it first inside a
 * `relative` container, the content after it.
 *
 *   <div className="relative isolate overflow-hidden">
 *     <Pattern kind="contours" tone="accent" fade="radial" />
 *     …
 *   </div>
 */
export function Pattern({ kind = 'dots', size, tone = 'muted', fade = 'none', seed = 1, animate = false, strength = 1, className, style }: PatternProps) {
  const sid = useSceneId('kpt');
  const play = useScenePlay<HTMLDivElement>('in-view');
  const s = Math.max(1, size ?? SIZE[kind]);
  const pid = `${sid}p`;

  /* tiled kinds: one tile, the layer is one period larger than its box so the drift never shows an edge */
  let period = 0;
  let tile: React.ReactNode = null;
  let drift: [number, number] = [0, 0];
  if (kind === 'dots') {
    period = s;
    tile = <circle cx={round(s / 2, 2)} cy={round(s / 2, 2)} r={round(Math.max(1.1, s / 16), 2)} style={{ fill: 'var(--pt-3)' }} />;
    drift = [s, s];
  } else if (kind === 'grid') {
    period = s * 4;
    tile = (
      <>
        <path d={[1, 2, 3].map((k) => `M${k * s + 0.5} 0V${period}M0 ${k * s + 0.5}H${period}`).join('')} style={{ stroke: 'var(--pt-1)', fill: 'none' }} shapeRendering="crispEdges" />
        <path d={`M0.5 0V${period}M0 0.5H${period}`} style={{ stroke: 'var(--pt-2)', fill: 'none' }} shapeRendering="crispEdges" />
        <rect x="-1.5" y="-1.5" width="4" height="4" style={{ fill: 'var(--pt-3)' }} />
      </>
    );
    drift = [period, period];
  } else if (kind === 'lines') {
    period = s * 6;
    tile = (
      <>
        <path d={[1, 2, 3, 4, 5].map((k) => `M0 ${k * s + 0.5}H${period}`).join('')} style={{ stroke: 'var(--pt-1)' }} shapeRendering="crispEdges" />
        <path d={`M0 0.5H${period}`} style={{ stroke: 'var(--pt-2)' }} shapeRendering="crispEdges" />
      </>
    );
    drift = [0, period];
  } else if (kind === 'diagonal') {
    period = s;
    tile = <path d={`M${round(s / 2, 2)} -1V${s + 1}`} style={{ stroke: 'var(--pt-2)', strokeWidth: 1 }} />;
    drift = [round(s * Math.SQRT2, 3), 0];
  } else if (kind === 'cross') {
    period = s;
    const c = round(s / 2, 2);
    const a = round(Math.max(2.5, s / 7), 2);
    tile = <path d={`M${round(c - a, 2)} ${c}H${round(c + a, 2)}M${c} ${round(c - a, 2)}V${round(c + a, 2)}`} style={{ stroke: 'var(--pt-3)', strokeWidth: 1.2, strokeLinecap: 'round', fill: 'none' }} />;
    drift = [s, s];
  } else if (kind === 'waves') {
    period = s * 4;
    const y = round(s / 2, 2);
    const a = round(s * 0.62, 2);
    tile = <path d={`M0 ${y}Q${s} ${round(y - a, 2)} ${s * 2} ${y}T${period} ${y}`} style={{ stroke: 'var(--pt-2)', strokeWidth: 1.1, fill: 'none' }} />;
    drift = [period, 0];
  }

  const levels = Math.max(5, Math.min(32, Math.round(340 / s)));
  const contours = useMemo(() => (kind === 'contours' ? contourPaths(seed, levels) : []), [kind, seed, levels]);

  const on = animate ? play.state : undefined;
  const css: string[] = [];
  let body: React.ReactNode = null;

  if (period) {
    const w = period;
    const h = kind === 'waves' ? s : period;
    const patternTransform = kind === 'diagonal' ? 'rotate(45)' : undefined;
    const pad = Math.ceil(Math.max(Math.abs(drift[0]), Math.abs(drift[1]), w, h)) + 2;
    css.push(`@keyframes ${sid}d{from{transform:translate3d(0,0,0)}to{transform:translate3d(${drift[0]}px,${drift[1]}px,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d ${round(Math.max(18, (Math.hypot(drift[0], drift[1]) / 2.2) * 1), 1)}s linear infinite}`);
    body = (
      <svg className={`${sid}m absolute`} style={{ inset: -pad, width: `calc(100% + ${pad * 2}px)`, height: `calc(100% + ${pad * 2}px)` }}>
        <defs>
          <pattern id={pid} width={w} height={h} patternUnits="userSpaceOnUse" patternTransform={patternTransform}>
            {tile}
          </pattern>
        </defs>
        <rect width="100%" height="100%" fill={`url(#${pid})`} />
      </svg>
    );
  } else if (kind === 'contours') {
    css.push(`@keyframes ${sid}d{0%{transform:scale(1.06) translate3d(0,0,0)}50%{transform:scale(1.1) translate3d(-1.2%,0.8%,0)}100%{transform:scale(1.06) translate3d(0,0,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 60s ease-in-out infinite}`);
    css.push(`@keyframes ${sid}s{0%,100%{opacity:.55}50%{opacity:1}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}x{animation:${sid}s 9s ease-in-out infinite}`);
    body = (
      <svg viewBox={`0 0 ${CW} ${CH}`} preserveAspectRatio="xMidYMid slice" className={`${sid}m absolute inset-0 size-full`} style={{ transform: 'scale(1.06)' }}>
        <g fill="none" strokeLinecap="round" strokeLinejoin="round">
          {contours.map((p, i) => (
            <path key={i} d={p.d} className={p.major ? `${sid}x` : undefined} style={{ stroke: p.major ? 'var(--pt-3)' : 'var(--pt-2)', strokeWidth: p.major ? 1.5 : 1, animationDelay: p.major ? `${-i * 0.7}s` : undefined }} vectorEffect="non-scaling-stroke" />
          ))}
        </g>
      </svg>
    );
  } else if (kind === 'rings') {
    const cx = round(CW * (0.35 + rand(seed, 1) * 0.4));
    const cy = round(CH * (0.3 + rand(seed, 2) * 0.4));
    const step = s * 1.4;
    const count = Math.ceil(Math.hypot(CW, CH) / step);
    css.push(`@keyframes ${sid}r{0%,100%{opacity:.45}30%{opacity:1}60%{opacity:.45}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}c{animation:${sid}r 7s ease-in-out infinite;animation-delay:calc(var(--i) * 0.22s)}`);
    body = (
      <svg viewBox={`0 0 ${CW} ${CH}`} preserveAspectRatio="xMidYMid slice" className="absolute inset-0 size-full">
        <g fill="none">
          {Array.from({ length: count }, (_, i) => {
            const major = i % 4 === 3;
            const dashed = !major && rand(seed, i + 7) > 0.72;
            return (
              <circle
                key={i}
                cx={cx}
                cy={cy}
                r={round(step * (i + 1), 1)}
                className={`${sid}c`}
                style={{ '--i': i, stroke: major ? 'var(--pt-3)' : 'var(--pt-2)', strokeWidth: major ? 1.4 : 1, strokeDasharray: dashed ? '2 7' : undefined } as React.CSSProperties}
                vectorEffect="non-scaling-stroke"
              />
            );
          })}
          <circle cx={cx} cy={cy} r="4" style={{ fill: 'var(--pt-3)' }} />
        </g>
      </svg>
    );
  } else if (kind === 'halftone') {
    // soft dots, weighted by a light field, then cut at half alpha: the dots grow where the field is strong
    // the light sits toward one side, so text in the middle stays on small dots
    const fx = round(rand(seed, 3) < 0.5 ? 0.1 + rand(seed, 5) * 0.2 : 0.7 + rand(seed, 5) * 0.2, 3);
    const fy = round(0.2 + rand(seed, 4) * 0.6, 3);
    const p = s;
    css.push(`@keyframes ${sid}h{0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(9%,-7%,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}h 34s ease-in-out infinite;transform-box:view-box}`);
    body = (
      <svg className="absolute inset-0 size-full" style={{ opacity: tone === 'accent' ? round(0.78 * strength, 2) : round(0.3 * strength, 2) }}>
        <defs>
          <radialGradient id={`${pid}d`}>
            <stop offset="0" style={{ stopColor: 'var(--pt-solid)' }} />
            <stop offset="1" style={{ stopColor: 'var(--pt-solid)', stopOpacity: 0 }} />
          </radialGradient>
          <pattern id={pid} width={p} height={p} patternUnits="userSpaceOnUse">
            <circle cx={p / 2} cy={p / 2} r={round(p * 0.7, 2)} fill={`url(#${pid}d)`} />
          </pattern>
          <radialGradient id={`${pid}l`} cx={fx} cy={fy} r="0.72">
            <stop offset="0" stopColor="#fff" />
            <stop offset="0.55" stopColor="#fff" stopOpacity="0.62" />
            <stop offset="1" stopColor="#fff" stopOpacity="0" />
          </radialGradient>
          <mask id={`${pid}m`} maskUnits="userSpaceOnUse" x="0" y="0" width="100%" height="100%">
            <rect className={`${sid}m`} x="-20%" y="-20%" width="140%" height="140%" fill={`url(#${pid}l)`} />
          </mask>
          <filter id={`${pid}f`} x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB">
            <feComponentTransfer>
              <feFuncA type="linear" slope="16" intercept="-7.4" />
            </feComponentTransfer>
          </filter>
        </defs>
        <g filter={`url(#${pid}f)`}>
          <g mask={`url(#${pid}m)`}>
            <rect width="100%" height="100%" fill={`url(#${pid})`} />
          </g>
        </g>
      </svg>
    );
  } else if (kind === 'noise') {
    const freq = round(Math.min(1.2, Math.max(0.25, 1.8 / s)), 3);
    css.push(`@keyframes ${sid}d{from{transform:translate3d(0,0,0)}to{transform:translate3d(240px,240px,0)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 40s steps(48) infinite}`);
    const url = grain(freq);
    body = (
      <div
        className={`${sid}m absolute`}
        style={{ inset: -242, background: 'var(--pt-solid)', opacity: tone === 'accent' ? round(0.5 * strength, 2) : round(0.2 * strength, 2), maskImage: url, WebkitMaskImage: url, maskSize: '240px 240px', WebkitMaskSize: '240px 240px' }}
      />
    );
  } else if (kind === 'gradient') {
    const fields = [
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 34%, transparent)' : 'color-mix(in oklab, var(--foreground) 11%, transparent)', w: 70 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 22%, color-mix(in oklab, var(--info) 24%, transparent))' : 'color-mix(in oklab, var(--foreground) 7%, transparent)', w: 58 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 14%, color-mix(in oklab, var(--foreground) 6%, transparent))' : 'color-mix(in oklab, var(--foreground) 9%, transparent)', w: 50 },
      { c: tone === 'accent' ? 'color-mix(in oklab, var(--primary) 20%, transparent)' : 'color-mix(in oklab, var(--foreground) 5%, transparent)', w: 44 },
    ];
    fields.forEach((_, i) => {
      css.push(`@keyframes ${sid}g${i}{0%,100%{transform:translate3d(0,0,0) scale(1)}50%{transform:translate3d(${round((rand(seed, i + 60) - 0.5) * 16, 1)}%,${round((rand(seed, i + 70) - 0.5) * 14, 1)}%,0) scale(${round(1.04 + rand(seed, i + 80) * 0.08, 3)})}}`);
      css.push(`.${sid}:not([data-kv="off"]) .${sid}g${i}{animation:${sid}g${i} ${round(22 + i * 5 + rand(seed, i + 90) * 6, 1)}s ease-in-out infinite}`);
    });
    body = fields.map((f, i) => {
      const x = round(rand(seed, i * 3 + 1) * 90 - 10, 1);
      const y = round(rand(seed, i * 3 + 2) * 80 - 15, 1);
      return (
        <div
          key={i}
          className={`${sid}g${i} absolute aspect-square rounded-full`}
          style={{ left: `${round(x - f.w / 2, 1)}%`, top: `${round(y - f.w / 2, 1)}%`, width: `${f.w}%`, background: `radial-gradient(closest-side, ${f.c}, transparent)` }}
        />
      );
    });
  } else if (kind === 'mesh') {
    const at = (i: number) => `${round(rand(seed, i * 2 + 101) * 100)}% ${round(rand(seed, i * 2 + 102) * 100)}%`;
    const colors =
      tone === 'accent'
        ? ['color-mix(in oklab, var(--primary) 78%, var(--background))', 'color-mix(in oklch, var(--primary) 55%, var(--info))', 'color-mix(in oklab, var(--primary) 30%, var(--background))', 'color-mix(in oklch, var(--primary) 60%, var(--success) 40%)', 'color-mix(in oklab, var(--primary) 45%, var(--foreground) 20%)']
        : ['color-mix(in oklab, var(--foreground) 16%, var(--background))', 'color-mix(in oklab, var(--foreground) 6%, var(--background))', 'color-mix(in oklab, var(--foreground) 24%, var(--background))', 'color-mix(in oklab, var(--foreground) 10%, var(--background))', 'color-mix(in oklab, var(--foreground) 3%, var(--background))'];
    const layers = colors.map((c, i) => `radial-gradient(${['60% 70%', '55% 60%', '70% 55%', '50% 65%', '65% 60%'][i]} at ${at(i)}, ${c}, transparent)`).join(',');
    css.push(`@keyframes ${sid}d{0%,100%{transform:rotate(0deg) scale(1.15)}50%{transform:rotate(${round(4 + rand(seed, 9) * 6, 1)}deg) scale(1.24)}}`);
    css.push(`.${sid}:not([data-kv="off"]) .${sid}m{animation:${sid}d 48s ease-in-out infinite}`);
    const url = grain(0.85);
    body = (
      <>
        <div className={`${sid}m absolute inset-0`} style={{ backgroundColor: 'var(--surface)', backgroundImage: layers, transform: 'scale(1.15)', opacity: round(Math.max(0.2, strength), 2) }} />
        <div className="absolute inset-0" style={{ background: 'var(--foreground)', opacity: 0.07, maskImage: url, WebkitMaskImage: url, maskSize: '240px', WebkitMaskSize: '240px' }} />
      </>
    );
  }

  return (
    <div
      ref={play.ref}
      aria-hidden
      data-kv={on}
      data-clip-ok
      className={cn(sid, 'pointer-events-none absolute inset-0 -z-10 overflow-hidden', className)}
      style={{ ...inks(tone, strength), ...FADE[fade], WebkitMaskImage: FADE[fade].maskImage, ...style }}
    >
      {animate && <style dangerouslySetInnerHTML={{ __html: SCENE_CSS + css.join('\n') }} />}
      {body}
    </div>
  );
}

Frequent questions

How do I install Pattern?

With aiw add @ai-website/pattern. The command puts components/ui/pattern.tsx into your project, the code is yours from then on. It brings along motion-scene. It needs no further npm packages.

Which variants does Pattern have?

8 variants, Dots, Grid, Lines, Contour lines, Grain, Light fields and mesh, Waves and Halftone.

Can an AI assistant build with Pattern?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Pattern with aiw_components and install it with aiw_add @ai-website/pattern.

Often searched

Jump to

Copied to the clipboard