Dirck Mulder
Backgrounds||4 min read

Build a Radar Background in React with WebGL

Render a sweeping radar animation with glowing rings, spokes, and fading trails using a WebGL fragment shader.

Radar immediately signals surveillance, detection, and data in motion. It is a classic metaphor for a reason. The Radar component brings that energy to React with a WebGL fragment shader that makes rings and sweeps look genuinely glowing and dimensional.

The final result

What we are building

A full-canvas radar display with concentric rings, radial spokes, and a rotating sweep beam. Everything is drawn per-pixel in GLSL using polar coordinate math. The sweep leaves behind a natural falloff as it rotates. Mouse interaction optionally shifts the center of the display.

Setting up

bash
npm install ogl
tsx
import { Renderer, Program, Mesh, Triangle } from 'ogl';
import { useEffect, useRef } from 'react';

Building the component

All the rendering happens in the fragment shader. The first step is converting screen coordinates to a centered, aspect-corrected space and then to polar coordinates:

glsl
vec2 st = gl_FragCoord.xy / uResolution.xy;
st = st * 2.0 - 1.0;
st.x *= uResolution.x / uResolution.y;

if (uEnableMouse) {
  vec2 mShift = (uMouse * 2.0 - 1.0);
  mShift.x *= uResolution.x / uResolution.y;
  st -= mShift * uMouseInfluence;
}

st *= uScale;

float dist = length(st);
float theta = atan(st.y, st.x);
float t = uTime * uSpeed;

dist and theta are the polar coordinates. Every visual element from here uses these two values.

The rings use fract to repeat at fixed intervals. The fractional part oscillates between 0 and 1 as you move outward, so subtracting 0.5 centers each ring:

glsl
float ringPhase = dist * uRingCount - t;
float ringDist = abs(fract(ringPhase) - 0.5);
float ringGlow = 1.0 - smoothstep(0.0, uRingThickness, ringDist);

Multiplying uRingCount by dist before fract spaces rings evenly in Cartesian distance. Subtracting t makes them appear to pulse outward over time.

Spokes use the same trick on the angular axis. The arc distance converts angular distance to approximate screen-space distance at a given radius, which keeps spoke thickness visually consistent regardless of where you are on the circle:

glsl
float spokeAngle = abs(fract(theta * uSpokeCount / TAU + 0.5) - 0.5) * TAU / uSpokeCount;
float arcDist = spokeAngle * dist;
float spokeGlow = (1.0 - smoothstep(0.0, uSpokeThickness, arcDist)) * smoothstep(0.0, 0.1, dist);

The sweep uses a power function on a sine wave. The base sin(uSweepLobes * theta + sweepPhase) produces a cosine-shaped beam. Raising it to uSweepWidth sharpens the beam significantly for higher power values:

glsl
float sweepPhase = t * uSweepSpeed;
float sweepBeam = pow(max(0.5 * sin(uSweepLobes * theta + sweepPhase) + 0.5, 0.0), uSweepWidth);

A radial fade darkens the edges of the display and prevents the rings from extending to the screen edges:

glsl
float fade = smoothstep(1.05, 0.85, dist) * pow(max(1.0 - dist, 0.0), uFalloff);
float intensity = max((ringGlow + spokeGlow + sweepBeam) * fade * uBrightness, 0.0);
vec3 col = uColor * intensity + uBgColor;

The OGL setup follows the same pattern as other OGL components: Triangle geometry, a Program with uniforms, and a frame loop:

tsx
function update(time: number) {
  animationFrameId = requestAnimationFrame(update);
  program.uniforms.uTime.value = time * 0.001;

  if (enableMouseInteraction) {
    currentMouse[0] += 0.05 * (targetMouse[0] - currentMouse[0]);
    currentMouse[1] += 0.05 * (targetMouse[1] - currentMouse[1]);
    program.uniforms.uMouse.value[0] = currentMouse[0];
    program.uniforms.uMouse.value[1] = currentMouse[1];
  }

  renderer.render({ scene: mesh });
}

How to use it

tsx
<div className="relative h-screen">
  <Radar
    color="#9f29ff"
    backgroundColor="#000000"
    ringCount={10}
    spokeCount={10}
    sweepSpeed={1.0}
    sweepWidth={2.0}
    brightness={1.0}
    enableMouseInteraction={true}
  />
  <div className="relative z-10">Your content</div>
</div>

Key takeaways

  • Polar coordinates (dist, theta) are the natural coordinate system for radar-style effects. All the visual elements map cleanly to distance and angle.
  • Using fract with a multiplier creates repeating patterns (rings, spokes) without any loops in the shader.
  • Raising a clamped sine wave to a power (pow(sin(...), sweepWidth)) sharpens a smooth beam into a tight directional sweep.