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
Variants
Details
What Pattern brings along, read from the registry and the source.
- Category
- Patterns and shapes
- Area
- Visuals
- 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
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The PatternProps interface from components/ui/pattern.tsx, with the comments from the code.
kindoptionalPatternKinddotsgridlinesdiagonalcrosswavescontoursnoisegradientmeshhalftonerings
What is drawn. Default dots.
sizeoptionalnumberScale 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.
toneoptionalPatternTonemutedaccent
muted (default) draws in the foreground at low strength, accent in the accent color
fadeoptionalPatternFadenoneradialtopbottomedges
Where the pattern fades out. top and bottom name the edge that fades. Default none.
seedoptionalnumberPicks positions and shapes for contours, halftone, rings, gradient and mesh
animateoptionalbooleanA very slow drift or shimmer, paused out of view, off with reduced motion
strengthoptionalnumber0 to 1, how strong the lines are. Default 1.
styleoptionalReact.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
'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.