A radar scope with range rings and a turning beam, where contacts flare as the beam passes over them, then fade.
Scanning
Customize
Usage
import { RadarSweep } from "@/components/motionkit/radar-sweep";
<RadarSweep className="h-[32rem] rounded-3xl">
<h1>Your hero</h1>
</RadarSweep>Motion
The exact curves this component runs, plotted with Motion's own maths.
Beam turn
Runs on its own
Contact flare
The beam passes a contact
Security, monitoring, logistics and defence-tech heroes, "searching" and empty states, and dashboards that watch for something.
The rings, crosshair and bearing ticks are one SVG. The beam is a conic gradient that's transparent for most of the turn, then brightens into a hard leading edge; the whole disc turns on a linear loop. Contact positions come from an integer hash, so they match on the server and in the browser. Each contact flashes and sends out a ring at the moment the beam reaches it, because its loop has the beam's duration and starts delayed by its angle as a share of a full turn. A dark radial gradient keeps the centre readable.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://motionkitui.com/r/radar-sweep.jsonnpm install motionSave as components/motionkit/radar-sweep.tsx.
"use client";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type RadarSweepProps = {
children?: ReactNode;
/** Colour of the beam and the blips. */
color?: string;
/** Seconds per full turn of the beam. */
duration?: number;
/** Number of range rings. */
rings?: number;
/** Number of contacts that light up as the beam passes. */
blips?: number;
className?: string;
};
// an integer hash, so blips land in the same places on the server and in every browser
const hash = (n: number) => {
let x = Math.imul(n ^ 0x9e3779b9, 0x85ebca6b);
x = Math.imul(x ^ (x >>> 13), 0xc2b2ae35);
return ((x ^ (x >>> 16)) >>> 0) / 4294967296;
};
/**
* A radar scope: range rings and a crosshair, a beam turning with a fading wake, and contacts that
* flare as the beam passes over them, then slowly fade.
*/
export function RadarSweep({ children, color = "#5FAEB4", duration = 5, rings = 4, blips = 7, className = "" }: RadarSweepProps) {
const reduceMotion = useReducedMotion();
const contacts = Array.from({ length: blips }, (_, index) => ({ angle: hash(index * 2 + 1) * 360, radius: 0.22 + hash(index * 2 + 2) * 0.7 }));
return (
<div className={`relative isolate overflow-hidden bg-[#0C1010] ${className}`}>
<div aria-hidden className="pointer-events-none absolute left-1/2 top-1/2 -z-10 aspect-square h-[150%] min-h-[28rem] -translate-x-1/2 -translate-y-1/2">
<svg viewBox="-100 -100 200 200" className="absolute inset-0 size-full overflow-visible">
{Array.from({ length: rings }, (_, index) => (
<circle key={index} r={((index + 1) / rings) * 100} fill="none" stroke="rgb(246 241 232 / 0.08)" strokeWidth={0.25} strokeDasharray={index === rings - 1 ? undefined : "1 1.5"} />
))}
<path d="M-100 0H100M0 -100V100" stroke="rgb(246 241 232 / 0.06)" strokeWidth={0.25} />
{Array.from({ length: 72 }, (_, index) => (
<line key={index} x1={0} y1={-100} x2={0} y2={index % 6 ? -98.5 : -96.5} stroke="rgb(246 241 232 / 0.12)" strokeWidth={0.25} transform={`rotate(${index * 5})`} />
))}
{contacts.map((contact, index) => {
const radians = (contact.angle * Math.PI) / 180;
const x = Math.sin(radians) * contact.radius * 100;
const y = -Math.cos(radians) * contact.radius * 100;
// each blip flares at the moment the beam's leading edge reaches its angle
const timing = { duration, repeat: Infinity, ease: "easeOut" as const, delay: (contact.angle / 360) * duration, times: [0, 0.03, 0.7, 1] };
return (
<g key={index}>
<motion.circle cx={x} cy={y} r={1.3} fill={color} initial={{ opacity: reduceMotion ? 0.5 : 0 }} animate={reduceMotion ? undefined : { opacity: [0, 1, 0.15, 0] }} transition={timing} />
{!reduceMotion && (
<motion.circle cx={x} cy={y} fill="none" stroke={color} strokeWidth={0.3} initial={{ r: 1.3, opacity: 0 }} animate={{ r: [1.3, 1.3, 6, 6], opacity: [0, 0.8, 0, 0] }} transition={{ ...timing, times: [0, 0.03, 0.35, 1] }} />
)}
</g>
);
})}
</svg>
{/* the beam: a conic wake that brightens to a hard leading edge at the top, then the whole disc turns */}
<motion.div
className="absolute inset-0 rounded-full"
style={{ background: `conic-gradient(from 0deg, transparent 0deg 270deg, color-mix(in oklab, ${color} 28%, transparent) 359deg, color-mix(in oklab, ${color} 80%, white) 359.6deg, transparent 360deg)` }}
initial={{ rotate: reduceMotion ? 45 : 0 }}
animate={reduceMotion ? undefined : { rotate: 360 }}
transition={{ duration, repeat: Infinity, ease: "linear" }}
/>
<div className="absolute inset-0 rounded-full bg-[radial-gradient(circle,transparent_55%,#0C1010_71%)]" />
</div>
{/* a soft dark centre keeps your content readable over the beam */}
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_45%_40%_at_center,rgb(12_16_16/0.75),transparent)]" />
{children}
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| color | string | "#5FAEB4" | Colour of the beam and contacts. |
| duration | number | 5 | Seconds per turn. |
| rings | number | 4 | Number of range rings. |
| blips | number | 7 | Number of contacts. |
| children | ReactNode | — | Your content, layered above. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://motionkitui.com/r/radar-sweep.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the beam stops at one angle and the contacts show faintly without flaring.
Contour Lines
Topographic lines that slowly shift like terrain, with a vignette for your content.
Circuit Traces
A circuit board around a clear centre: traces fan out from every side, and pulses of light race along them at scattered times.
Aurora Background
Drifting, blurred color fields for hero sections and empty states.