Skip to content
UI Kit
Accent color

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.

4 variantsOne of 6 components in 3D backgrounds

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.

Variants

Hero in depthNew in 1.2

A deep field of rising particles behind a centered headline.

Drifting bandNew in 1.2

Particles drift sideways through a full width band of figures.

Featured cardNew in 1.2

A dense, fine field inside one card of a grid.

Slow drift behind a call to actionNew in 1.2

Large, slow particles falling softly around the closing panel.

Details

What Particle field brings along, read from the registry and the source.

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

Props

The ParticleFieldProps interface from components/ui/particle-field.tsx, with the comments from the code. Also takes every prop of ShaderLayerProps.

  • densityoptional
    number

    Particles per layer, 1 is the designed density, 2 twice as many

  • layersoptional
    number

    Depth layers on desktop, 2 to 6, default 5. Phones draw about half.

  • directionoptional
    number

    Direction of the drift in degrees, 90 rises, default 100

  • sizeoptional
    number

    Size of the particles, 1 is the designed size

  • glowoptional
    boolean

    Light 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
components/ui/particle-field.tsx
'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.

Often searched

Jump to

Copied to the clipboard