Skip to content
UI Kit
Accent color

3D wave background

A wavy surface in perspective, drawn as lines or dots with WebGL. Glides toward the viewer, crests catch the accent, the pointer raises the waves under it.

4 variantsOne of 6 components in 3D backgrounds

A surface that glides toward you

A wave field draws a wavy surface in perspective, as lines or as dots, that glides toward the viewer. It gives a page motion and depth at the same time and leaves the upper half free for the copy.

Hero above the waves sets the copy above a field of lines that glides toward the viewer. Dotted band lays a low field of dots under a full width band of figures. Featured card fills the card with tall waves like a sound wave. Behind a call to action brings a calm sea of dots up to the middle of the closing panel.

Lines feel precise and technical, dots feel lighter, pick one style per page. The card version suits audio, voice and data products, where the wave shape tells the story by itself.

Variants

Hero above the wavesNew in 1.2

The copy stands above a field of lines that glides toward the viewer.

Dotted bandNew in 1.2

A low field of dots under a full width band of figures.

Featured cardNew in 1.2

Tall waves fill the feature card like a sound wave.

Behind a call to actionNew in 1.2

A calm sea of dots reaches up to the middle of the closing panel.

Details

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

Variants
4
Version
1.0.0 · since 1.2.0
File
components/ui/wave-field.tsx
Brings along
utilskitwebgl
npm packages
None
Languages
Texts come through props
Studio
Adjust in code

Props

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

  • patternoptional
    WaveFieldPattern

    linesdots

    lines draws each row as a line, dots as a grid of points

  • rowsoptional
    number

    Rows on desktop, 12 to 64, default 44. Each pixel only looks at the rows near its own depth, so more rows cost nothing extra.

  • horizonoptional
    number

    Height of the horizon as share of the box from the bottom, default 0.55

  • amplitudeoptional
    number

    Wave height, 1 is the designed height

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, 128 lines
components/ui/wave-field.tsx
'use client';

import { cn } from '@/lib/kit';
import { ShaderBackdrop, ShaderCanvas, type ShaderLayerProps } from '@/components/ui/webgl';

/*
 * A wavy surface seen in perspective, drawn as lines or dots. Rows sit at
 * depths that grow geometrically toward the horizon, every row is lifted by
 * the waves at its depth and projected onto the screen. The field glides
 * toward the viewer, crests catch the accent, far rows fade into haze. The
 * pointer raises the waves under it. Phones draw fewer rows.
 */

export type WaveFieldPattern = 'lines' | 'dots';

export interface WaveFieldProps extends ShaderLayerProps {
  /** lines draws each row as a line, dots as a grid of points */
  pattern?: WaveFieldPattern;
  /** Rows on desktop, 12 to 64, default 44. Each pixel only looks at the rows near its own depth, so more rows cost nothing extra. */
  rows?: number;
  /** Height of the horizon as share of the box from the bottom, default 0.55 */
  horizon?: number;
  /** Wave height, 1 is the designed height */
  amplitude?: number;
}

const FRAG = `
uniform float u_rows;uniform float u_horizon;uniform float u_amp;uniform float u_dots;
float wave(float x,float z,float t){
return (sin(x*1.15+t*.9+z*.55)*.55+sin(x*.53-t*.6+z*1.35)*.35+sin(x*2.1+z*.8-t*1.3)*.12)*u_amp;
}
void main(){
float m=min(u_res.x,u_res.y);
vec2 p=vec2(gl_FragCoord.x-.5*u_res.x,gl_FragCoord.y)/m;
float yh=u_res.y/m*u_horizon;
float px=1./m;
float f=yh*.55;
float t=u_time*.5;
float mx=(u_mouse.x-.5)*u_res.x/m;
float n=floor(u_rows*mix(.6,1.,u_quality));
float ph=fract(u_time*.08+u_scroll*.6);
vec3 col=vec3(0.);float al=0.;
float zf=f/max(yh-p.y,.002);
float ic=floor(log(max(zf/.45,1.))/log(30.)*n-1.+ph);
for(int j=0;j<15;j++){
float fi=ic-7.+float(j);if(fi<0.||fi>=n)continue;
float k=(fi+1.-ph)/n;
float z=.45*pow(30.,k);
float xw=p.x*z/f;
float bump=u_hover*.55*exp(-(p.x-mx)*(p.x-mx)*3.)*u_amp;
float h=wave(xw,z,t)*.32+bump*.3;
float y=yh-(1.-h)*f/z;
float dy=abs(p.y-y);
if(dy>.08)continue;
float far=k;
float w=px*mix(1.5,.75,far);
float v;
if(u_dots>.5){
float sp=.55;
float dx=(fract(xw/sp+.5)-.5)*sp*f/z;
float r=px*mix(2.6,.9,far);
v=smoothstep(r+px,r-px*.5,length(vec2(dx,p.y-y)));
}else{
v=smoothstep(w+px,w-px*.4,dy);
}
float fade=smoothstep(.88,.35,far)*smoothstep(0.,.08,k);
float crest=smoothstep(.0,.3,h);
vec3 c=mix(u_c3,u_c0,clamp(crest*.85+bump*.4+(1.-u_dark)*.35,0.,1.));
float a=v*fade*(.42+.58*crest);
col+=c*a;al+=a;
}
float glow=exp(-abs(p.y-yh)*7.)*.2*mix(.5,1.,u_dark)*smoothstep(yh+.18,yh-.02,p.y);
col+=u_c0*glow;al+=glow;
float kk=mix(.62,1.,u_dark)*u_intensity;
al=clamp(al*kk,0.,1.);
gl_FragColor=vec4(min(col*kk,vec3(1.)),al);
}`;

/* Still picture: a floor of lines tilted back with CSS, fading into the horizon */
function Still({ pattern, horizon }: { pattern: WaveFieldPattern; horizon: number }) {
  const ink = 'color-mix(in oklab, var(--primary) 55%, var(--foreground))';
  const bg =
    pattern === 'dots'
      ? `radial-gradient(circle, ${ink} 0 1.5px, transparent 2.2px) 0 0 / 26px 26px`
      : `repeating-linear-gradient(0deg, ${ink} 0 2px, transparent 2px 22px)`;
  return (
    <div className="absolute inset-x-0 bottom-0 overflow-hidden [perspective:420px]" style={{ height: `${Math.round(horizon * 100)}%` }}>
      <div
        className={cn('absolute inset-x-[-60%] bottom-[-10%] top-0 origin-bottom')}
        style={{ background: bg, transform: 'rotateX(64deg)', maskImage: 'linear-gradient(to top, #000 10%, transparent 92%)', WebkitMaskImage: 'linear-gradient(to top, #000 10%, transparent 92%)', opacity: 0.9 }}
      />
      <div className="absolute inset-x-0 top-0 h-px" style={{ boxShadow: '0 0 60px 14px color-mix(in oklab, var(--primary) 26%, transparent)' }} />
    </div>
  );
}

/**
 * A gliding wave surface in perspective behind any content. With children it
 * renders a box, without children only the layer for a `relative` parent.
 */
export function WaveField({ intensity = 1, speed = 1, colors, interactive = true, pattern = 'lines', rows = 44, horizon = 0.55, amplitude = 1, fallback, scrim = 'none', scrimStrength, scrimColor, children, className, contentClassName, layerClassName }: WaveFieldProps) {
  const h = Math.min(0.95, Math.max(0.15, horizon));
  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) 30%, var(--foreground))']}
          uniforms={{ u_rows: Math.max(12, Math.min(64, Math.round(rows))), u_horizon: h, u_amp: amplitude, u_dots: pattern === 'dots' ? 1 : 0 }}
          speed={speed}
          intensity={intensity}
          maxDpr={2}
          phoneDpr={1.5}
          pointer={interactive}
          fallback={fallback ?? <Still pattern={pattern} horizon={h} />}
          className={layerClassName}
        />
      }
    >
      {children}
    </ShaderBackdrop>
  );
}

Frequent questions

How do I install Wave field?

With aiw add @ai-website/wave-field. The command puts components/ui/wave-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 Wave field have?

4 variants, Hero above the waves, Dotted band, Featured card and Behind a call to action.

Can an AI assistant build with Wave field?

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

Often searched

Jump to

Copied to the clipboard