Skip to main content

Feedback

LevelMeter

A small live input level: a row of bars that light from the start edge as a microphone picks up sound, so the person can tell it hears them before anyone else does (before joining a call, in a device menu, in a "test your microphone" row). The app feeds value, the microphone's linear amplitude from 0 to 1, as often as about 15 times a second, and the meter maps it to its bars on a decibel scale, so ordinary speech lights about half of them. To get the amplitude, read an AnalyserNode's samples with getFloatTimeDomainData(samples) and take their RMS: Math.sqrt(samples.reduce((sum, sample) => sum + sample * sample, 0) / samples.length).

Microphone level
Menu row
Microphone level in a menu
-30

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/level-meter#v1.3.10

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/level-meter

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/level-meter#v1.3.10 ourostack/ouro-ui/level-meter-tests#v1.3.10
import { LevelMeter } from "@/components/ouro/level-meter/level-meter";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<LevelMeter label="Microphone level" value={rms} />
<LevelMeter label="Microphone level" hideLabel value={rms} size="sm" bars={5} />
<LevelMeter label="Microphone level" value={rms} muted={micMuted} />

Usage

The bars move as an audio meter's do: they rise within about 50 ms and fall over about 350 ms, so they hold between syllables. Every bar is the row's full height: lit bars are filled in the tone and unlit ones are a quiet track with no outline. Nothing moves or scales; a bar's colours cross-fade as it lights and goes out, the same with reduced motion. With more contrast unlit bars are outlined, and under forced colours each bar keeps an edge and a lit one is Highlight.

With muted the lit bars dim to the muted foreground and still follow the level, so a person who speaks while muted sees that the microphone hears them and that nobody else does.

It is a meter named by its label, from 0 to 100, with its level in words ("Quiet", "Speaking", "Loud", or "Muted") and a value at the middle of the word's band (10, 50 or 90; 0 while muted). What it reports is the loudest level of the last 1.5 seconds, so a person who speaks and then moves to the meter hears "Speaking", and its ARIA values change only when the word does, at most twice a second, so a screen reader has nothing new to repeat while the person speaks. It is no live region: screen readers read it when the person goes to it, and it never announces the level as it changes.

A new value re-renders the meter once. Between values it re-renders only when a bar lights or goes out or the word changes, and the page changes only for those bars and, when the word changes, the reading. The bars that keep their state are memoised.

Its agent side, levelMeterAx in level-meter-styles.ts, declares no agent reads and no actions: audio levels stay with the person.

Use instead when

  • Use Progress instead to show how far along a task is, and Spinner for a wait you can't measure.

Props

Type: LevelMeterProps.

bars
Type: number
Default: 8
How many bars to draw, a whole number from 3 to 24. A number outside that, or with a fraction, draws the nearest one.
className
Type: string
Layout only: margins, grid placement (mt-2, ms-auto). Appearance classes warn in development; use tone and size.
hideLabel
Type: boolean
Hides the label visually; screen readers still use it.
id
Type: string
The meter's id, for aria-describedby elsewhere on the page.
label (required)
Type: string
What the meter measures: "Microphone level". It names the meter for screen readers.
muted
Type: boolean
The microphone is muted: the lit bars dim to the muted foreground, whatever the tone, and still follow value, so the person can tell the microphone hears them while nobody else does. Screen readers hear "Muted" (valueText.muted) and a value of 0, whatever the level.
ref
Type: Ref<HTMLDivElement>
Reaches the rendered element.
size
Type: "sm" | "md" | "lg"
Default: "md"
The meter's size: bars 12px, 16px or 24px high. sm fits a menu row.
tone
Type: "primary" | "secondary" | "destructive" | "success" | "warning"
Default: "success"
Color role from the theme for the lit bars, named after its token. Unlit bars are always a quiet track in the input colour, so a silent meter is quiet whatever its tone. secondary fills lit bars in the muted foreground, as muted does.
value (required)
Type: number
The microphone's linear amplitude now, from 0 (silence) to 1 (full scale): the RMS of the samples an AnalyserNode gives, fed up to about 15 times a second. The meter maps it to its bars on a decibel scale from -60 dBFS (no bar) to 0 dBFS (every bar), so ordinary speech, around -40 to -20 dBFS, lights 3 to 5 of 8 bars. An amplitude below 0 or not a number shows as silence, and one above 1 as full scale. To get it: analyser.getFloatTimeDomainData(samples), then Math.sqrt(samples.reduce((sum, sample) => sum + sample * sample, 0) / samples.length).
valueText
Type: LevelMeterValueText
Default: { quiet: "Quiet", speaking: "Speaking", loud: "Loud", muted: "Muted" }
The words screen readers hear for the loudest level of the last 1.5 seconds: below -48 dBFS, up to -12 dBFS, and from there, and the word while muted. Translate them with the rest of your interface.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • color: Not a prop. Ouro UI uses tone: <LevelMeter tone="success">.
  • level: Not a prop. The amplitude is value: <LevelMeter value={rms}>.
  • max: Not a prop. value is an amplitude from 0 to 1: <LevelMeter value={rms}>.
  • segments: Not a prop. Ouro UI uses bars: <LevelMeter bars={8}>.

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.