Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Minimap radar

New

A game HUD minimap: player arrow and view cone, pins that slide along the rim, a turning north tab and sensor pings.

minimap-radar/minimap-radar.tsx
Live · interactive
Time 09:10 AMTemperature +41 °F

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • A corner map in a game HUD that shows, without opening the big map, which way the player faces and where their pins are.
  • Pins that may be anywhere in the world: near ones sit on the map, far ones ride the rim and point the way.
  • A heading-up map that turns with the player, with a north tab on the rim; or north-up for players who keep their bearings.
  • Telling the player a target is close without a number: a sensor that lights up and pings faster as the pin nears.
  • A small status chip that belongs to the map, such as the game clock and the temperature.

When not to use

  • The full world map the player plans a route on. Use a world map with zoom, pan and a legend; this shows a few hundred metres.
  • A single objective direction along the top of the screen. Use a compass strip, which takes less room.
  • Locations on a real map in an app (stores, deliveries). Use a web map with real tiles and controls.
  • A distance the player must read exactly, such as 37 m to the target. Put a label on a waypoint marker in the scene.
  • Enemies tracked in real time in a competitive game. Use a radar with its own threat markers and rules; these pins are the player’s own.

You can add this component with the shadcn CLI or by hand:

Into components/minimap-radar of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/minimap-radar.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

corner-map.tsx
import { MinimapRadar } from './minimap-radar';const pins = [  { id: 'camp', x: 600, y: 470, label: 'Camp' },  { id: 'peak', x: 300, y: 720, label: 'Peak' },];export function CornerMap() {  return <MinimapRadar variant="terrain" pins={pins} controls={false} autoplay={false} size={180} />;}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

The round map in the corner of a game screen. The arrow at its centre is the player and the soft cone is where the camera looks; yellow pins sit on the map when near and ride the rim in a dashed ring when out of range, sliding along it as the player turns. A grey N tab orbits the edge while the map turns with the player, or stays put in north-up mode. A sensor badge lights up and pings faster as a pin gets closer, and a chip under the disc keeps the game clock and the temperature. Walk it with the arrow keys, on a navy grid or a terrain map.

  • The ground is a canvas drawn at devicePixelRatio after every React commit, so it moves in the same paint as the SVG on top. It is clipped to the disc, then rotated by the map’s rotation, scaled to the range (160 world units across the radius by default) and translated so the player sits at the centre.
  • The world is invented: periodic value noise in five octaves (8 to 128 cells across a 1024-unit square that repeats both ways) gives a height field. From it, one terrain texture (six ochre land bands, darker contour edges, blue-grey water with a shore band, dark wood clumps and two pale roads) and one transparent contour texture for the navy map are painted once, a few rows per frame, so the page never stalls.
  • The navy map is #06203a with those contours at low alpha, a grid of lines every 72 units and small crosses at the quarter points, all drawn as one-device-pixel strokes so they stay sharp while turning, and a radial darkening over its outer tenth.
  • Everything else is SVG in a 260-unit frame where the disc’s radius is 100: the camera cone (a 41° wedge with a radial yellow gradient, softly blurred), the arrow, the pins, the N tab, the sensor badge and the ping rings.
  • Heading-up (the default) rotates the map by the heading, so the arrow points up and the N tab orbits the rim, its letter kept upright; north-up keeps the map still and turns the arrow. Switching swings the map to its new rotation over 420 ms (cubic ease-out) instead of jumping. The camera trails the character with a 160 ms exponential settle, so after a turn the cone catches up a moment later.
  • Pins live in world units. Within range they are placed on the map; beyond it they are clamped to the rim in their direction, and a dashed ring (radius 20.8 units, dash 8.6 / 4.47) grows round them. Crossing the range, the ring spins and shrinks into the pin in 260 ms and the pin pops (scale 0.4 to 1.22 to 1); closer than 9 units, a pin is reached: it swells and scales away under a yellow burst ring.
  • The sensor tracks the nearest pin within 2.4 ranges: its badge turns from grey to ice cyan and a cyan ring sweeps from the player to the rim in 1.1 s, every 2.2 s at the edge of its reach down to every 0.55 s close by; the badge’s three arcs flash white in turn with each ping.
  • The chip runs the game clock at a game minute per second and shows the temperature (warmest mid-afternoon, colder at night and up high), its needle easing to each new reading on a nine-segment cold, mild and hot dial.
  • Untouched and on screen, an autopilot steers with the same keys a reader has: it seeks a pin until it is reached, switches to terrain, goes north-up for a turn, drops a pin ahead and reaches it, switches back and restores the pins.
PropTypeDefaultDescription
variant'navy' | 'terrain''navy'The ground: a navy grid with faint contours, or a terrain map.
rotatebooleantrueHeading-up: the map turns with the player and the N tab orbits. False keeps north up and turns the arrow.
pinsMinimapPin[]three pinsPins as { id, x, y, label? } in world units (the world repeats every 1024; the player starts at 512, 560).
rangenumber160How far the disc’s radius reaches, in world units; pins beyond it ride the rim.
timenumber550The game clock at the start, in minutes since midnight (550 is 09:10 AM).
headingnumber—Where the player faces at the start, in degrees clockwise from north. Setting it opens the radar turned that way and turns the autopilot off.
timeScalenumber1Game minutes per real second; 0 stops the clock.
unit'C' | 'F''F'The temperature’s unit in the chip.
sensorbooleantrueShows the sensor badge and its pings.
controlsbooleantrueShows the buttons under the radar: turn, walk, drop a pin, terrain, north up.
sizenumber—The disc’s diameter in px. Without it, 176, then 200 on a stage 400 px wide or more and 232 from 640 px.
onPinReached(pin: MinimapPin) => void—Called when the player reaches a pin, which then clears.
autoplaybooleantrueWhile on screen and untouched, an autopilot plays a short run; any real input hands it over.
colorScheme'light' | 'dark' | 'system'—Light or dark north tab, rim and buttons, or the reader’s (system); without it, the page’s color-scheme.
classNamestring—Added to the root element.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

minimap-radar.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useId,  useLayoutEffect,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,  type PointerEvent as ReactPointerEvent,} from 'react';import { useAutoplay } from './use-autoplay';import { WORLD, paintTextures, temperatureAt, type Textures } from './world';import styles from './minimap-radar.module.css';/* * Minimap radar: the round map in a corner of a game HUD that tells the player, without opening * the big map, which way they face, where their pins are and which way is north. * * - The disc is a canvas at devicePixelRatio: the ground under the player, either a navy grid *   with faint contour lines or a terrain map (world.ts paints both textures from one invented,
world.ts
// Copyright (c) 2026 Bruno Delorence. MIT License./* * The invented world under the minimap: a square of WORLD units (metres) that repeats in both * directions, so the player can walk forever. Its height comes from periodic value noise (each * octave's lattice wraps at the world's edge), and from the height come the terrain colours, the * contour lines, the water and the temperature. Two textures are painted from it, in slices so * the page never stalls: the terrain map (ochre land, blue-grey water, dark woods, pale roads, * contour lines) and, for the navy variant, the same contours as faint lines on transparency. *//** The world's side, in units; everything wraps at it. */export const WORLD = 1024;/** Below this height is water. */const WATER = 0.33;function hash(x: number, y: number, seed: number) {  let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 2246822519);  h = Math.imul(h ^ (h >>> 13), 1274126177);  return ((h ^ (h >>> 16)) >>> 0) / 4294967295;}/** Periodic value noise: `cells` lattice cells across the world, smooth, from 0 to 1. */function noise(x: number, y: number, cells: number, seed: number) {
minimap-radar.module.css
/* Minimap radar (minimap-radar.tsx): the radar disc and its time and temperature chip, alone in the   middle of a plain stage, with a row of buttons under it.   The disc is --d wide (176px; 200px on a stage 400px wide or more, 232px from 640px, or the size   prop). The SVG layers are 1.3 × --d, centred on it, in a 260-unit frame where the disc's radius   is 100, so the north tab, the rim pins and the sensor badge can stand out past the edge.   The disc, its pins and the chip are dark and glowing on any page, as a HUD draws them over a   world; what touches the page (the north tab, the rim's hairline, the shadow, the buttons)   follows the color scheme through light-dark() tokens. No filter blurs the art itself: glows are   drop shadows behind sharp SVG shapes. */.root {  /* The radar's own colours, the same on any scheme. */  --navy: #06203a;  --navy-edge: #081724;  --ground: #3a2f10;  --yellow: #ffe460;  --pin: #fff07a;  --pin-line: #e8a80f;  --pin-dash: rgb(255 240 178 / 0.88);  --glow: rgb(255 212 50 / 0.85);  --cyan: #3cd3fc;  --cyan-glow: rgb(60 211 252 / 0.55);
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. `visible` and `reduced` are also returned, so * the component can pause its own clock offscreen and calm its effects. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {
  • The radar is one image (role="img") whose label says the mode, the map, the facing and the nearest pin with its distance and side, such as “Facing north-east. 3 pins; nearest Ridge, 139 metres ahead left”; the drawing is aria-hidden.
  • The stage takes focus: then the arrow keys or W A S D walk and turn, and P drops a pin. The buttons under it do the same: the four movement buttons are held with the pointer or with Space or Enter on the focused button, and Terrain and North up are toggle buttons with aria-pressed.
  • The time and temperature are plain text with visually hidden labels.
  • A pin coming into range, a pin reached, a pin dropped and a mode change are announced through a polite live region, only for the reader’s own play, never for the autopilot.
  • Reduced motion: no autopilot, no pings, pops, bursts or boot animation, and mode switches are instant; walking still moves the map, since that is the reader’s own input.
  • The clock and the demo pause offscreen and in a hidden tab, and the autopilot lets go of its keys the moment the reader takes over.