# Radar Sweep

> A radar scope with range rings and a turning beam, where contacts flare as the beam passes over them, then fade.

Category: Backgrounds · Free · Updated 2026-10-23 · Live demo: https://motionkitui.com/components/radar-sweep

## When to use it

Security, monitoring, logistics and defence-tech heroes, "searching" and empty states, and dashboards that watch for something.

## How it works

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.

## Installation

```bash
npx shadcn@latest add https://motionkitui.com/r/radar-sweep.json
```

Or copy `components/motionkit/radar-sweep.tsx` below. Requires react, motion and Tailwind CSS v4.

## Props

| 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. |

## Accessibility

- **Keyboard: Not applicable.** Decorative, nothing to focus.
- **Screen readers: Pass.** The scope is hidden from assistive tech; only your content is read.
- **Reduced motion: Pass.** With reduced motion on, the beam stops at one angle and the contacts show faintly without flaring.
- **Colour contrast: Pass.** A soft dark centre keeps ivory text at 14.7:1 or better, even as the beam passes behind it.

## Source

```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>
  );
}
```
