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
npm install oglimport { 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:
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:
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:
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:
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:
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:
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
<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
fractwith 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.