Particle background
Glowing particles in up to six depth layers, drawn with WebGL. Near ones large and soft, far ones fine, the pointer spreads the layers and lights its surroundings.
Depth you can feel with the pointer
A particle field fills the space behind your copy with glowing particles in several depth layers. They move a little with the pointer, so the page gains depth without a single 3D model, and a scrim keeps the text in front clear.
Hero in depth sets a deep field of rising particles behind a centered headline. Drifting band lets particles drift sideways through a full width band of figures. Featured card puts a dense, fine field inside one card of a grid. Slow drift behind a call to action lets large, slow particles fall softly around the closing panel.
Particles feel technical and calm at the same time, they suit products in data, security and AI. Keep the field sparse behind long text and dense only in a card, then it supports the copy instead of competing with it.
Details
What Particle field brings along, read from the registry and the source.
- Category
- 3D backgrounds
- Area
- Visuals
- Variants
- 4
- Version
- 1.0.0 · since 1.2.0
- File
- components/ui/particle-field.tsx
- Brings along
- utilskitwebgl
- 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 ParticleFieldProps interface from components/ui/particle-field.tsx, with the comments from the code. Also takes every prop of ShaderLayerProps.
densityoptionalnumberParticles per layer, 1 is the designed density, 2 twice as many
layersoptionalnumberDepth layers on desktop, 2 to 6, default 5. Phones draw about half.
directionoptionalnumberDirection of the drift in degrees, 90 rises, default 100
sizeoptionalnumberSize of the particles, 1 is the designed size
glowoptionalbooleanLight around the pointer, default true
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
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 100 lines
'use client';
import { GLSL_NOISE, ShaderBackdrop, ShaderCanvas, type ShaderLayerProps } from '@/components/ui/webgl';
/*
* A field of glowing particles in several depth layers. Near layers are
* larger, softer and faster, far layers fine and slow, so the field reads as
* space. The pointer pushes the layers apart (parallax) and lights the
* particles around it. Each particle lives in its own grid cell, so the cost
* stays flat however many there are. Phones draw fewer layers.
*/
export interface ParticleFieldProps extends ShaderLayerProps {
/** Particles per layer, 1 is the designed density, 2 twice as many */
density?: number;
/** Depth layers on desktop, 2 to 6, default 5. Phones draw about half. */
layers?: number;
/** Direction of the drift in degrees, 90 rises, default 100 */
direction?: number;
/** Size of the particles, 1 is the designed size */
size?: number;
/** Light around the pointer, default true */
glow?: boolean;
}
const FRAG = `${GLSL_NOISE}
uniform float u_density;uniform float u_layers;uniform vec2 u_dir;uniform float u_size;uniform float u_glow;
void main(){
float m=min(u_res.x,u_res.y);
vec2 p=(gl_FragCoord.xy-.5*u_res)/m;
vec2 mp=(u_mouse-.5)*u_res/m;
float n=max(2.,floor(u_layers*mix(.5,1.,u_quality)+.5));
vec3 col=vec3(0.);float al=0.;
for(int i=0;i<6;i++){
float fi=float(i);if(fi>=n)break;
float d=fi/max(1.,n-1.);
float sc=mix(26.,7.,d)*u_density;
vec2 q=p*sc-u_dir*u_time*mix(.25,1.1,d)+vec2(fi*17.3,fi*9.1);
q+=(mp*mix(.15,1.2,d)*u_hover+vec2(0.,(u_scroll-.5)*mix(.6,3.,d)))*sc*.08;
vec2 id=floor(q),f=fract(q)-.5;
vec2 h=hash2(id+fi*31.);
if(h.x>mix(.55,.22,d))continue;
vec2 o=(h-.5)*.5;
float r=mix(.0016,.0075,d)*sc*u_size*(.6+.8*fract(h.y*7.31));
float tw=.65+.35*sin(u_time*(1.+h.x*3.)+h.y*40.);
float dist=length(f-o);
float blur=r*mix(.35,.9,d*d);
float edge=smoothstep(.5,.32,max(abs(f.x),abs(f.y)));
float dt=smoothstep(r+blur,r-blur*.6,dist)+exp(-dist*dist/(r*r*5.))*.3*edge;
vec2 wp=(id+.5+o)/sc;
float near=u_glow*u_hover*exp(-dot(wp-mp,wp-mp)*30.);
vec3 c=mix(u_c0,u_c3,(1.-h.y)*mix(.25,1.,u_dark))+near*.4*u_dark;
float a=dt*tw*mix(.5,1.,d)*(.7+near*1.4);
col+=c*a;al+=a;
}
float fade=mix(.7,1.,u_dark)*u_intensity;
al=clamp(al*fade,0.,1.);
col=col*fade;
gl_FragColor=vec4(min(col,vec3(1.)),al);
}`;
/** The still picture: three sizes of dots, small and dense, larger and sparse */
const FALLBACK = [
'radial-gradient(circle at 30% 40%, color-mix(in oklab, var(--primary) 70%, transparent) 0 2.5px, transparent 3.5px) 0 0 / 97px 83px',
'radial-gradient(circle at 60% 70%, color-mix(in oklab, var(--primary) 45%, var(--foreground) 10%) 0 1.5px, transparent 2.5px) 0 0 / 53px 61px',
'radial-gradient(circle at 20% 20%, color-mix(in oklab, var(--foreground) 30%, transparent) 0 1px, transparent 1.6px) 0 0 / 29px 31px',
].join(', ');
/**
* Glowing particles in depth behind any content. With children it renders a
* box, without children only the layer for a `relative` parent.
*/
export function ParticleField({ intensity = 1, speed = 1, colors, interactive = true, density = 1, layers = 5, direction = 100, size = 1, glow = true, fallback, scrim = 'center', scrimStrength, scrimColor, children, className, contentClassName, layerClassName }: ParticleFieldProps) {
const rad = (direction * Math.PI) / 180;
return (
<ShaderBackdrop
scrim={scrim}
scrimStrength={scrimStrength}
scrimColor={scrimColor}
className={className}
contentClassName={contentClassName}
layer={
<ShaderCanvas
fragment={FRAG}
colors={colors ?? ['var(--primary)', 'var(--primary)', 'var(--primary)', 'color-mix(in oklab, var(--primary) 50%, var(--foreground))']}
uniforms={{ u_density: density, u_layers: Math.min(6, Math.max(2, layers)), u_dir: [Math.round(Math.cos(rad) * 1000) / 1000 * 0.12, Math.round(Math.sin(rad) * 1000) / 1000 * 0.12], u_size: size, u_glow: glow ? 1 : 0 }}
speed={speed}
intensity={intensity}
maxDpr={1.5}
pointer={interactive}
fallback={fallback ?? FALLBACK}
mask="radial-gradient(ellipse 90% 85% at 50% 50%, #000 55%, transparent)"
className={layerClassName}
/>
}
>
{children}
</ShaderBackdrop>
);
}Frequent questions
How do I install Particle field?
With aiw add @ai-website/particle-field. The command puts components/ui/particle-field.tsx into your project, the code is yours from then on. It brings along webgl. It needs no further npm packages.
Which variants does Particle field have?
4 variants, Hero in depth, Drifting band, Featured card and Slow drift behind a call to action.
Can an AI assistant build with Particle field?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Particle field with aiw_components and install it with aiw_add @ai-website/particle-field.