Skip to content
UI Kit
Accent color

3D dot globe

A turning sphere of evenly spaced dots, ray traced with WebGL. Lit front, faint back, a few dots glowing in the accent and a halo around it. Tilts with the pointer.

4 variantsOne of 6 components in 3D backgrounds

A globe of light points that keeps turning

A dot sphere turns a globe of evenly spaced dots behind or beside your copy, ray traced with WebGL, with accent sparks and a soft halo. It says global, connected or network without a map and without a single picture to load.

Hero with sphere beside puts the copy on the left and the turning sphere on the right, on phones it rests below the copy. Horizon lets a large sphere rise from the bottom edge like a planet with the copy above it. Featured card lets a sphere peek into the top corner of the feature card. Behind a call to action frames the closing panel with a big, quiet sphere whose rim glows around the copy.

Pick the split layout when the copy is long and the horizon when the headline is short and strong. The sparks take your accent color, so keep other accents on the same screen to a minimum.

Variants

Hero with sphere besideNew in 1.2

Copy on the left, the sphere turns on the right. On phones it rests below the copy.

HorizonNew in 1.2

A large sphere rises from the bottom edge like a planet, the copy stands above it.

Featured cardNew in 1.2

A sphere peeks into the top corner of the feature card.

Behind a call to actionNew in 1.2

A big, quiet sphere frames the closing panel, its rim glows around the copy.

Details

What Dot sphere brings along, read from the registry and the source.

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

Props

The DotSphereProps interface from components/ui/dot-sphere.tsx, with the comments from the code. Also takes every prop of ShaderLayerProps.

  • placementoptional
    DotSpherePlacement

    centerrighthorizoncorner

    center behind the copy, right next to it (from 900 px box width, below the copy on narrower boxes), horizon rising from the bottom edge, or corner peeking in at the top right (for cards)

  • sizeoptional
    number

    Size, 1 is the designed size

  • ringsoptional
    number

    Rings of dots from pole to pole, default 30

  • sparksoptional
    number

    Share of dots that glow in the accent, 0 to 0.5, default 0.12

  • halooptional
    number

    Strength of the halo, default 1

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, 143 lines
components/ui/dot-sphere.tsx
'use client';

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

/*
 * A sphere made of evenly spaced dots, ray traced per pixel. Rings of dots
 * run from pole to pole, each ring with as many dots as fit its circumference,
 * so the spacing stays even. Front dots are lit, back dots shine through
 * faintly, a few dots glow in the accent and a halo surrounds the sphere. The
 * pointer tilts it, the scroll turns it further. Abstract on purpose: no land,
 * no map, no arcs (that is the Globe).
 */

export type DotSpherePlacement = 'center' | 'right' | 'horizon' | 'corner';

export interface DotSphereProps extends ShaderLayerProps {
  /** center behind the copy, right next to it (from 900 px box width, below the copy on narrower boxes), horizon rising from the bottom edge, or corner peeking in at the top right (for cards) */
  placement?: DotSpherePlacement;
  /** Size, 1 is the designed size */
  size?: number;
  /** Rings of dots from pole to pole, default 30 */
  rings?: number;
  /** Share of dots that glow in the accent, 0 to 0.5, default 0.12 */
  sparks?: number;
  /** Strength of the halo, default 1 */
  halo?: number;
}

const PLACE: Record<DotSpherePlacement, number> = { center: 0, right: 1, horizon: 2, corner: 3 };

const FRAG = `${GLSL_NOISE}
uniform float u_place;uniform float u_size;uniform float u_rings;uniform float u_sparks;uniform float u_halo;
const float PI=3.14159265;
vec3 dots(vec3 n,float aa,float back){
float N=u_rings;
float lat=asin(clamp(n.y,-1.,1.));float lon=atan(n.z,n.x);
float ri=floor((lat/PI+.5)*N+.5);float la=(ri/N-.5)*PI;
float cnt=max(1.,floor(2.*N*cos(la)+.5));
float li=floor((lon/(2.*PI)+.5)*cnt+.5);float lo=(li/cnt-.5)*2.*PI;
vec3 dc=vec3(cos(la)*cos(lo),sin(la),cos(la)*sin(lo));
float ang=length(n-dc);
float rr=PI/N*.21;
float m=smoothstep(rr+aa,rr-aa,ang);
float h=hash(vec2(ri,mod(li,cnt))+3.1);
float spark=step(h,u_sparks)*(.55+.45*sin(u_time*1.6+h*90.));
return vec3(m,m*spark,back);
}
void main(){
float m=min(u_res.x,u_res.y);
vec2 p=(gl_FragCoord.xy-.5*u_res)/m;
float hw=.5*u_res.x/m,hh=.5*u_res.y/m;
vec2 c=vec2(0.);float R=.34;
if(u_place>.5&&u_place<1.5){if(u_res.x/u_dpr>=900.){c=vec2(hw*.5,0.);R=.36;}else{R=min(.36,150.*u_dpr/m);c=vec2(0.,-hh+R+.07);}}
if(u_place>1.5&&u_place<2.5){R=max(hw*1.05,.95);c=vec2(0.,-hh-R*.62);}
if(u_place>2.5){R=min(min(.42,hh*.62),100.*u_dpr/m);c=vec2(hw-R*.72,hh-R*.68);}
R*=u_size;
vec2 d=p-c;float r2=dot(d,d);float len=sqrt(r2);
float spin=u_time*.12+u_scroll*1.4+(u_mouse.x-.5)*.7*u_hover;
float tilt=-.38+(u_mouse.y-.5)*.5*u_hover;
float cs=cos(spin),sn=sin(spin),ct=cos(tilt),st=sin(tilt);
vec3 col=vec3(0.);float al=0.;
vec3 base=mix(u_c3,u_c0,(1.-u_dark)*.45);vec3 acc=u_c0;
if(len<R){
float z=sqrt(R*R-r2);float face=z/R;
float aa=1.3/(R*m)/max(face,.18);
for(int k=0;k<2;k++){
float s=k==0?1.:-1.;
vec3 n=vec3(d,s*z)/R;
n=vec3(n.x,n.y*ct-n.z*st,n.y*st+n.z*ct);
n=vec3(n.x*cs-n.z*sn,n.y,n.x*sn+n.z*cs);
vec3 g=dots(n,aa,float(k));
float lit=(k==0?(.32+.68*face):.16*(1.-face*.4))*mix(.62,1.,u_dark);
vec3 dc=mix(base,acc,min(1.,g.y*1.4));
float a=g.x*lit*(1.+g.y*.8);
col+=dc*a;al+=a;
}
float body=.06*face*u_dark;col+=acc*body*.6;al+=body;
}
float edge=len-R;
float halo=u_halo*(exp(-max(edge,0.)*7.)*.5*step(0.,edge)+exp(-abs(edge)*60.)*.35);
halo*=mix(.55,1.,u_dark);
col+=acc*halo;al+=halo;
float k=mix(.85,1.,u_dark)*u_intensity;
al=clamp(al*k,0.,1.);
gl_FragColor=vec4(min(col*k,vec3(1.)),al);
}`;

/* Still picture: a disc of dots with a soft halo, placed like the live sphere */
const SPOT: Record<DotSpherePlacement, string> = {
  center: 'left-1/2 top-1/2 aspect-square w-[min(68%,68vh)] -translate-x-1/2 -translate-y-1/2',
  right: 'max-lg:left-1/2 max-lg:top-[62%] max-lg:aspect-square max-lg:w-[min(80%,48vh)] max-lg:-translate-x-1/2 lg:right-[6%] lg:top-1/2 lg:aspect-square lg:w-[min(42%,72vh)] lg:-translate-y-1/2',
  horizon: 'left-1/2 top-[64%] aspect-square w-[max(110%,62rem)] -translate-x-1/2',
  corner: 'right-0 top-0 aspect-square h-[62%] translate-x-[28%] -translate-y-[32%]',
};

function Still({ placement }: { placement: DotSpherePlacement }) {
  const dots = 'radial-gradient(circle, color-mix(in oklab, var(--primary) 45%, var(--foreground)) 0 1.6px, transparent 2.2px) 0 0 / 14px 14px';
  return (
    <div className={cn('absolute rounded-full', SPOT[placement])} style={{ boxShadow: '0 0 80px 6px color-mix(in oklab, var(--primary) 30%, transparent)' }}>
      <div
        className="absolute inset-0 rounded-full"
        style={{
          background: dots,
          maskImage: 'radial-gradient(circle at 42% 38%, #000 0%, rgb(0 0 0 / 0.55) 55%, rgb(0 0 0 / 0.2) 71%, transparent 71.5%)',
          WebkitMaskImage: 'radial-gradient(circle at 42% 38%, #000 0%, rgb(0 0 0 / 0.55) 55%, rgb(0 0 0 / 0.2) 71%, transparent 71.5%)',
        }}
      />
    </div>
  );
}

/**
 * A turning sphere of dots behind or beside any content. With children it
 * renders a box, without children only the layer for a `relative` parent.
 */
export function DotSphere({ intensity = 1, speed = 1, colors, interactive = true, placement = 'center', size = 1, rings = 30, sparks = 0.12, halo = 1, fallback, scrim = 'none', scrimStrength, scrimColor, children, className, contentClassName, layerClassName }: DotSphereProps) {
  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) 28%, var(--foreground))']}
          uniforms={{ u_place: PLACE[placement], u_size: size, u_rings: Math.max(8, Math.min(60, Math.round(rings))), u_sparks: Math.min(0.5, Math.max(0, sparks)), u_halo: halo }}
          speed={speed}
          intensity={intensity}
          maxDpr={2}
          phoneDpr={1.5}
          pointer={interactive}
          fallback={fallback ?? <Still placement={placement} />}
          className={layerClassName}
        />
      }
    >
      {children}
    </ShaderBackdrop>
  );
}

Frequent questions

How do I install Dot sphere?

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

Which variants does Dot sphere have?

4 variants, Hero with sphere beside, Horizon, Featured card and Behind a call to action.

Can an AI assistant build with Dot sphere?

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

Often searched

Jump to

Copied to the clipboard