Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Noise meter

New

A stealth HUD dial whose violet trace ripples calmly while you sneak and spikes and glows when you are loud.

noise-meter/noise-meter.tsx
Live · interactive

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 stealth readout in a game HUD: how much noise the player makes while sneaking, read at a glance from the corner of the eye.
  • A level that changes many times a second and only needs to be felt, not read as a number: quiet, moving or loud.
  • A signal that should only be on screen while it matters: the dial comes up while sneaking and goes away on standing up.
  • One-off bursts on top of a steady level, such as a landing or a dropped item, fired by counting up burst.
  • A live audio or activity cue in a game-like interface, where a glowing trace fits the art.

When not to use

  • A level the reader must read exactly or compare. Use a bar or a gauge with a number and a scale.
  • A microphone or volume input in a settings screen. Use a slider with a live level bar.
  • A health, stamina or charge resource that empties and refills. Use a gauge or a wheel built for it.
  • A history of a signal over time. Use a sparkline or a chart, which keep the past on screen.
  • A calm product interface. The scrolling, glowing trace pulls the eye; use a status dot or a badge.

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

Into components/noise-meter of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/noise-meter.json

Either way it needs React 19.2 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

stealth-hud.tsx
import { useState } from 'react';import { NoiseMeter, type NoiseState } from './noise-meter';export function StealthHud({ speed, sneaking }: { speed: number; sneaking: boolean }) {  const state: NoiseState = speed > 6 ? 'loud' : speed > 0.5 ? 'moving' : 'quiet';  const [landings, setLandings] = useState(0);  // Call setLandings((n) => n + 1) when the player lands a jump.  return <NoiseMeter bare size={64} state={state} shown={sneaking} burst={landings} />;}

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 small dark dial a game shows while the player sneaks, so they can tell at a glance whether they will be heard. Quiet is a low, calm ripple; moving adds a livelier wobble; loud fills the dial with tall jagged spikes and a violet glow, with a quick punch as it turns loud. A thump (a landing, a dropped item) spikes it for a moment, and standing up hides it. Use it bare in a HUD of your own, or with its chips to drive it.

  • The dial is one SVG in a 50-unit box (the size of the dial in the kit), so it draws crisp at any size. The trace is a single path rebuilt every frame: a slow sine ripple, always there, plus a row of spike samples, up and down in turn, that scrolls in from the right like an oscilloscope.
  • The samples keep only their shape (a ragged height and a slow swell that leaves gaps between bursts); the noise level of the moment scales the whole row, so the trace answers within a frame or two. The path has a point on every sample, so no spike loses its tip, and the spikes taper off towards the rim.
  • The level moves like an audio meter: a 50 ms attack and a 450 ms release. A thump holds it at full for 180 ms, then lets it fade over about a second.
  • The glow is a blurred copy of the trace, under the sharp one, plus a soft violet bloom behind it; the trace itself is never blurred. Both read one custom property, --glow, which follows the level but falls slower (700 ms), so a warning lingers a beat after the noise stops.
  • Turning loud, or a thump, gives the dial a punch played by the Web Animations API: a dip to 95%, a pop to 107% and a settle, in 420 ms. Showing and hiding the dial are CSS transitions on scale and opacity, with an overshoot on the way in and none on the way out.
  • The frame loop runs only while the dial is shown, on screen and in a visible tab. While untouched, a short sneak plays on its own: crouch, creep, walk, creep, run, walk, land, stand up.
PropTypeDefaultDescription
state'quiet' | 'moving' | 'loud'—How loud the player is. Set it to control the meter (the demo is then off); without it the meter keeps its own, driven by its controls.
defaultState'quiet' | 'moving' | 'loud''quiet'The state an uncontrolled meter starts in.
onStateChange(state: NoiseState) => void—Called when the reader picks a state with the controls.
levelnumber—A precise noise level from 0 to 1, overriding the state’s own (0.06, 0.46 and 1): spikes start above 0.1 and the glow above 0.5.
shownboolean—Whether the dial is up (the player is sneaking). Set it to control it.
defaultShownbooleantrueWhether an uncontrolled dial starts up.
onShownChange(shown: boolean) => void—Called when the reader shows or hides the dial with the controls.
burstnumber0Count it up to fire a one-off spike, like a landing or a dropped item: full noise for 180 ms, then a fade.
sizenumber144The dial’s width in px; the kit draws it at 50. Everything inside scales with it.
barebooleanfalseJust the dial: no controls, to place in a HUD of your own.
labelstring'Noise'The meter’s accessible name, also used in its announcements.
autoplaybooleantrueWhile on screen and untouched, a short sneak plays on its own. Off when state or shown is set, and in bare mode.
colorScheme'light' | 'dark' | 'system'—Light or dark chips (and a plain surface of that scheme behind them), or the reader’s system setting. Without it the component follows the page’s color-scheme. The dial is the same on both, as a game HUD is: a dark disc with a soft drop shadow and a faint rim, so it reads on either.
classNamestring—Extra class for the root.

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

noise-meter.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useEffect,  useEffectEvent,  useId,  useRef,  useState,  type CSSProperties,  type KeyboardEvent as ReactKeyboardEvent,} from 'react';import { useAutoplay } from './use-autoplay';import styles from './noise-meter.module.css';/* * Noise meter: the stealth readout of a game HUD. While the player sneaks, a small dark dial shows * how much noise they make: a calm violet ripple when quiet, a livelier wobble when moving, and * tall jagged spikes with a violet glow when loud. Standing up hides it. * * - The dial is one SVG in a 50-unit box (the kit's 50 px), drawn at any size. The wave is a *   single path rebuilt every frame from two parts: a slow sine ripple that is always there, and *   a row of spike samples, alternately up and down, that scrolls in from the right like an
noise-meter.module.css
/* Noise meter (noise-meter.tsx): a dark dial with a violet wave, shown on its own on the page,   with small neutral chips under it to drive it.   The dial itself is the same on both schemes, as a game HUD is: a near-black disc and a violet   trace. To read on any background it carries its own edge, as HUD elements do: a soft drop   shadow on light, a faint light rim on dark. The chips follow the scheme. Colours are the tokens   below, light-dark(light, dark) where the two differ: the component takes the color-scheme it   inherits from the page, or the one its colorScheme prop sets (data-color-scheme).   Sizes in the dial are in the drawing's units (the kit's 50 px dial); --size scales it all. The   frame loop writes two custom properties on the dial: --glow (0–1, the violet bloom and the   trace's halo) and --spike (0–1, how much of the noise is spikes). */.root {  /* The dial: disc, trace, and the bloom behind a loud trace. Fixed on both schemes. */  --disc: #121212;  --trace: #be5cbc;  --bloom: #9a33a0;  /* The dial's own edge, so it reads on a light page and on a dark one. */  --rim: rgb(0 0 0 / 0.5);  --drop: rgb(20 10 24 / 0.22);  /* The chips: small neutral controls that follow the scheme. */  --chip: #ffffff;  --chip-hover: #f1eff3;
use-autoplay.ts
// Copyright (c) 2026 Bruno Delorence. MIT License.'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` alone (on screen, tab shown) is also * returned, so a live drawing can pause offscreen whoever is driving it. */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);
  • The dial is a meter (role="meter") named by label, with its value as a percentage and its state as text (“Quiet”, “Moving”, “Loud”). When hidden it is also hidden from screen readers.
  • The levels are a radio group: Tab reaches the checked one, arrow keys, Home and End move and choose. Sneak is a toggle button (aria-pressed) and Thump a plain button; all have a visible focus ring.
  • Changes the reader makes are announced in a polite live region (“Noise: loud”, “Noise meter hidden”); the demo’s own changes are not, so it never chatters.
  • The drawing is decorative (aria-hidden). Under reduced motion nothing scrolls or pops: each state shows a still drawing, changes are instant, a thump shows the loud drawing for 600 ms, and the demo never plays.