Skip to content
animorio
IT
All components

The radar menu

Menus, SVG, Hover, TextGSAP, CSS onlyMediumCompressed weight: Demo 8.3 KBGSAP 28 KBFreeDownload the zip

In the minimal style: the menu of a small harbour as a radar screen on black. Pressing Menu, the range rings, the cross and the ticks of the bezel are drawn under the button and the beam starts to turn: every link lights up as the beam passes over it and then slowly fades, like phosphor. The pointer takes the beam and holds it on the link you are looking at; the page you are on is in signal orange.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>The radar menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the menu of a small harbour, drawn as a radar screen. At rest only the Menu button. Pressed, the range rings
         and the bezel are drawn under it and the beam starts to turn: every link lights up as the beam passes over
         it and fades again slowly, like phosphor. The pointer (or the finger, or the arrow keys) takes the beam and
         holds it on a link; the page you are on is marked in signal orange -->
    <div class="radar" data-frame>
      <button class="toggle" type="button" aria-expanded="false" aria-controls="radar-menu" data-toggle data-autoclic>
        <span class="toggle__icon" aria-hidden="true">
          <svg viewBox="-10 -10 20 20" width="20" height="20">
            <circle class="toggle__ring" r="8.2"></circle>
            <circle class="toggle__ring toggle__ring--in" r="4.2"></circle>
            <line class="toggle__beam" data-icon-beam x1="0" y1="0" x2="0" y2="-8.2"></line>
          </svg>
        </span>
        <span class="toggle__words" aria-hidden="true">
          <span class="toggle__word" data-word-open>Menu</span>
          <span class="toggle__word" data-word-close>Close</span>
        </span>
        <span class="sr" data-toggle-name>Open the menu</span>
      </button>

      <nav class="menu" id="radar-menu" aria-label="Main" data-menu inert>
        <div class="scope" data-scope>
          <svg class="scope__grid" viewBox="-100 -100 200 200" aria-hidden="true" data-grid>
            <g data-rings>
              <circle class="scope__ring" r="24" pathLength="1"></circle>
              <circle class="scope__ring" r="48" pathLength="1"></circle>
              <circle class="scope__ring" r="72" pathLength="1"></circle>
              <circle class="scope__ring scope__ring--edge" r="96" pathLength="1"></circle>
            </g>
            <line class="scope__cross" data-cross x1="0" y1="0" x2="0" y2="-96" pathLength="1"></line>
            <line class="scope__cross" data-cross x1="0" y1="0" x2="96" y2="0" pathLength="1"></line>
            <line class="scope__cross" data-cross x1="0" y1="0" x2="0" y2="96" pathLength="1"></line>
            <line class="scope__cross" data-cross x1="0" y1="0" x2="-96" y2="0" pathLength="1"></line>
            <g data-ticks></g>
            <path class="scope__north" data-north d="M -3 -103 L 0 -98.5 L 3 -103"></path>
          </svg>
          <div class="scope__sweep" aria-hidden="true" data-sweep></div>
          <div class="scope__beam" aria-hidden="true" data-beam></div>

          <ul class="blips" data-blips>
            <li class="blip" data-blip data-angle="36" data-range="0.6"><a class="blip__link" href="#moorings" data-link>
              <span class="blip__mark" aria-hidden="true" data-mark><svg viewBox="-5 -5 10 10"><path d="M -4 0 H 4 M 0 -4 V 4"></path></svg></span>
              <span class="blip__text"><span class="blip__word" data-word>Moorings</span><span class="blip__note" data-note>Berths and fuel</span></span>
            </a></li>
            <li class="blip" data-blip data-angle="122" data-range="0.6"><a class="blip__link" href="#school" aria-current="page" data-link>
              <span class="blip__mark" aria-hidden="true" data-mark><svg viewBox="-5 -5 10 10"><path d="M -4 0 H 4 M 0 -4 V 4"></path></svg></span>
              <span class="blip__text"><span class="blip__word" data-word>School</span><span class="blip__note" data-note>Learn to sail</span></span>
            </a></li>
            <li class="blip" data-blip data-angle="196" data-range="0.5"><a class="blip__link" href="#boatyard" data-link>
              <span class="blip__mark" aria-hidden="true" data-mark><svg viewBox="-5 -5 10 10"><path d="M -4 0 H 4 M 0 -4 V 4"></path></svg></span>
              <span class="blip__text"><span class="blip__word" data-word>Boatyard</span><span class="blip__note" data-note>Repairs, winter</span></span>
            </a></li>
            <li class="blip" data-blip data-angle="264" data-range="0.5"><a class="blip__link" href="#tides" data-link>
              <span class="blip__mark" aria-hidden="true" data-mark><svg viewBox="-5 -5 10 10"><path d="M -4 0 H 4 M 0 -4 V 4"></path></svg></span>
              <span class="blip__text"><span class="blip__word" data-word>Tides</span><span class="blip__note" data-note>Today and weekly</span></span>
            </a></li>
            <li class="blip" data-blip data-angle="330" data-range="0.36"><a class="blip__link" href="#contact" data-link>
              <span class="blip__mark" aria-hidden="true" data-mark><svg viewBox="-5 -5 10 10"><path d="M -4 0 H 4 M 0 -4 V 4"></path></svg></span>
              <span class="blip__text"><span class="blip__word" data-word>Contact</span><span class="blip__note" data-note>Harbour office</span></span>
            </a></li>
          </ul>
        </div>
      </nav>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

31 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The radar menu” from Animorio and fit it into my site

What it does

On a black background there is only a pill button with a hairline outline: inside it, a tiny radar drawn in outline and the word Menu. With the mouse over it the little radar's hand makes a quarter turn and the word widens and gains weight. Pressed, Menu slides away and Close comes in, and the screen is drawn underneath: four range rings closing one after the other from the centre to the edge, the cross starting from the middle, seventy-two ticks appearing round the bezel and an orange mark at north. Then the beam starts, a hairline with a pale trail fading behind it: the first turn is fast and slows down to one turn every 3.6 seconds, and the button's hand turns with it. The links, Moorings, School, Boatyard, Tides and Contact, are small crosses with a word beside them, scattered over the screen at different ranges from the centre, and they cannot be seen until the beam finds them. When the beam passes over a link it lights up fully: the word turns full and wide, a note appears under it (Berths and fuel, Learn to sail…) and the cross turns 45 degrees; then in two and a half seconds it fades until it is barely visible. With the pointer inside the screen the beam follows it on a spring, and over a link it stops there and keeps it lit; leaving, it starts turning again. The page you are on, School, is orange. Choosing a link, its word is pressed for a moment, turns orange and the screen closes in reverse: the links go dark, the ticks vanish anticlockwise and the rings are wiped back towards the centre.

How it works

The screen being drawn

The rings and the cross are SVG circles and lines with pathLength 1 and stroke-dasharray 1 1: the timeline takes stroke-dashoffset from 1 to 0, the rings 0.09 seconds apart from the centre to the edge. The bezel's 72 ticks are made by the script (one every 5 degrees, longer every 30) and appear clockwise with a stagger of DRAW·0.7/72. The stroke width comes from the diameter (200/d viewBox units), so it stays one pixel at any size.

The beam and its trail

The beam is a one pixel div with its transform-origin at the bottom, the trail a round div with a conic-gradient from transparent at 292 degrees to a pale veil at 360: both turn by the same angle, written on every frame in a requestAnimationFrame loop with its step capped at 1/30 of a second. The first turn runs 2.6 times faster and slows down with power2.out to SWEEP seconds a turn.

The links' light

Every link has a light from 0 to 1: when the beam comes within BEAM degrees it goes up to 1, then drops by dt/FADE every frame. The light drives the opacity (from REST to 1), Mona Sans' weight and width in CSS variables read by font-variation-settings, the note underneath and the turn of the cross. Until the beam finds it for the first time a link is invisible and cannot be clicked.

The pointer takes the beam

Inside the screen the pointer's angle (atan2 from the centre) becomes the beam's target, and the beam goes there on a spring: its speed grows by SPRING[0] times the distance and drops by SPRING[1] times the speed, always the short way round. Over a link, or with the keyboard focus on it, the target is the link's angle and its light stays at 1. Leaving, the beam starts again from still and gets back to speed in 0.7 seconds.

Choosing and closing

The click presses the word to 0.95 and back, moves aria-current (and the orange) and after 0.75 seconds the screen closes in reverse: links and beam go dark, the ticks vanish from the last to the first, the cross and the rings go back to stroke-dashoffset 1 from the edge towards the centre.

Keyboard and the preview's tour

The button has aria-expanded and aria-controls, the nav is inert while closed. Enter or Space open it and take the focus to the current page; the arrows move from link to link in the order the beam meets them, Tab goes round the button and the links, Esc closes. The preview's click (detail 0, with no key just pressed) makes a tour: it opens, lets the beam turn, holds it on two links and chooses one.

Values to change

NameFileDefaultWhat it changes
SWEEPscript.js3.6The seconds the beam takes for a full turn.
FADEscript.js2.4The seconds a link takes to fade after the beam has passed it.
BEAMscript.js7How many degrees either side of the beam light a link up.
RESTscript.js0.3How much a link shows between two passes of the beam (0 to 1).
WEIGHTscript.js[300, 640]The weight of a word in the dark and right under the beam (100 to 900).
WIDTHscript.js[82, 114]The width of a word in the dark and under the beam (75 to 125).
SPRINGscript.js[150, 18]The spring that takes the beam to the pointer: how stiff, and how much it is held back.
DRAWscript.js1.1The seconds the rings take to be drawn when the menu opens.

Try them in the Playground

When to use it

For the menu of anyone dealing with the sea, navigation and instruments: a marina, a sailing school, a boatyard, a tides or weather app, a studio making surveying tools or software. It suits four, five or six short links on a dark background; the note underneath says in a few words what lies behind it.

On phones

The screen takes its diameter from the window's width and height (between 170 and 380 pixels), so the button and the open radar always fit; the words shrink with the screen, and below a 300 pixel diameter the notes are left out. The same animations: the beam turns and lights the links, and a finger on the screen takes it where you want; a tap on a link chooses it.

With reduced motion

No turning beam and no rings being drawn: opening, the screen and all the links fade in, already half lit; under the pointer or with the focus a link gains weight and width. Closing, everything fades out.

Pitfalls

  • With vector-effect: non-scaling-stroke together with pathLength the stroke is drawn only halfway: the width is worked out from the screen's diameter instead (200/d viewBox units) in a CSS variable.
  • Links left of the centre grew towards the edge and came out of the screen: they have the blip--left class, the word on the left of the cross and the text aligned right, so they grow towards the centre.
  • A link the beam had not found yet was invisible but still clickable: until it has been seen it has pointer-events none.

Libraries

GSAP 3.15.0, Mona Sans (font variabile, con gli assi del peso e della larghezza) 5.3.0.

Compressed with gzip, the three demo files weigh 8.3 KB; the libraries: GSAP 28 KB.

Testing

Tested on 23 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components