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).
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-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then 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.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/level-meterIts 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.10import { 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
Progressinstead to show how far along a task is, andSpinnerfor 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; usetoneandsize. hideLabel- Type:
boolean - Hides the label visually; screen readers still use it.
id- Type:
string - The meter's id, for
aria-describedbyelsewhere 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.
smfits 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.
secondaryfills lit bars in the muted foreground, asmuteddoes. value(required)- Type:
number - The microphone's linear amplitude now, from 0 (silence) to 1 (full scale): the RMS of the samples an
AnalyserNodegives, 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), thenMath.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 usestone:<LevelMeter tone="success">.level: Not a prop. The amplitude isvalue:<LevelMeter value={rms}>.max: Not a prop.valueis an amplitude from 0 to 1:<LevelMeter value={rms}>.segments: Not a prop. Ouro UI usesbars:<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.
