Voice Glow
Install: npm install voice-glow@0.3.0
When to use
Visualize a simulated or permissioned audio level around content.
Key native props
| Prop | Meaning |
|---|---|
look | glow, dots or lines. Color palettes do not recolor the monochrome dots/lines surfaces. |
level | Manual 0–1 number or getter. A getter avoids React updates for every audio frame. |
stream | A provided MediaStream takes precedence over level. Obtain it only after an explicit user gesture and permission. |
active | Fade out and stop analysis; paused holds the current frame instead. idle=0 removes resting glow in the silent example. |
theme | Pass app light/dark; auto follows system preference. onLevel runs per frame: avoid setState on every call. |
Interaction and state checks
Move the simulated level from zero to one, switch looks, toggle active and pause. Test keyboard range input and ensure no microphone prompt appears just by opening this preview.
Integration boundaries
No recording, transcription or voice backend is included. The example never calls useMicrophone/getUserMedia; supplying a real stream is a separate application responsibility.
Example files and host setup
Copy the component file and shared.tsx from examples in the source panel, keeping their relative locations; do not copy only the upstream implementation. Install the pinned npm package and use its own export in a browser component. Examples need no Tailwind or competing theme provider, read host html language/theme and provide reduced-motion/graphics fallbacks. Native implementation references are for reading; import via npm rather than copying an incomplete engine. Showcase labels are not new native props. Upstream detailed prose remains English; no full-translation claim is made.
Runnable usage
"use client";
import { useState } from "react";
import { VoiceBeam, type VoiceBeamLook } from "voice-glow";
import { DemoFrame, GraphicsGuard, control, demoTheme, panel, t } from "./shared";
export default function Example({ variant = "Default" }: { variant?: string }) {
const [look, setLook] = useState<VoiceBeamLook>(variant === "Dots" ? "dots" : variant === "Lines" ? "lines" : "glow"), [level, setLevel] = useState(variant === "Silent" ? 0 : .45), [active, setActive] = useState(variant !== "Inactive"), [paused, setPaused] = useState(variant === "Paused");
return <DemoFrame status={`look=${look}, level=${level.toFixed(2)}, active=${active}, paused=${paused}`} controls={<>
<label>{t("Look", "外观")} <select style={control} aria-label={t("Voice appearance", "声音外观")} value={look} onChange={e => setLook(e.target.value as VoiceBeamLook)}>{["glow", "dots", "lines"].map(s => <option key={s}>{s}</option>)}</select></label>
<label>{t("Simulated level", "模拟音量")} <input aria-label={t("Simulated voice level", "模拟声音音量")} type="range" min="0" max="1" step="0.01" value={level} onChange={e => setLevel(Number(e.target.value))} /></label>
<button type="button" style={control} aria-pressed={active} onClick={() => setActive(!active)}>{active ? t("Deactivate", "关闭特效") : t("Activate", "开启特效")}</button>
<button type="button" style={control} aria-pressed={paused} onClick={() => setPaused(!paused)}>{paused ? t("Resume", "继续动画") : t("Pause", "暂停动画")}</button>
</>}><GraphicsGuard><VoiceBeam look={look} level={level} active={active} paused={paused} idle={variant === "Silent" ? 0 : .23} theme={demoTheme()} style={{ width: "100%", maxWidth: 320 }}><div style={panel}>{t("Simulated audio, no microphone", "模拟声音,不使用麦克风")}</div></VoiceBeam></GraphicsGuard></DemoFrame>;
}
Preview states
Default, Dots, Lines, Silent, Inactive, Paused
Demo state and mock data do not provide a backend service. Inspect the component props before wiring real data.
Voice
voice-glow on npm.
Sound-reactive glow for React. A centered, colorful beam along the bottom edge of any element — a chat input, a search bar, a card — that rises and blooms with the intensity of a voice. Feed it a microphone stream, or drive it yourself.
It is the voice-shaped sibling of border-beam's line type: the same three-layer construction (edge stroke, inner light, blurred bloom), the same eight palettes, but the glow sits still at the center and its height follows the sound instead of traveling.
Install
npm install voice-glow
Quick start
import { VoiceBeam, useMicrophone } from 'voice-glow';
function Composer() {
const mic = useMicrophone();
return (
<>
<VoiceBeam stream={mic.stream}>
<div style={{ padding: 32, borderRadius: 20, background: '#1d1d1d' }}>
Listening…
</div>
</VoiceBeam>
<button onClick={mic.state === 'live' ? mic.stop : mic.start}>
{mic.state === 'live' ? 'Stop' : 'Listen'}
</button>
</>
);
}
The component wraps your content and overlays the glow. It auto-detects the border-radius of the first child element. Call mic.start() from a click: browsers only grant the microphone — and Safari only starts audio — inside a user gesture.
Types
The glow is authored for a ~350px chat input. type retunes its geometry for other hosts — every geometry prop below still overrides the preset:
<VoiceBeam type="pill"> {/* a ~150×44 recording pill: glow pulled in, shallow bend, thin band */}
<RecordingPill />
</VoiceBeam>
<VoiceBeam type="mobile"> {/* the bottom of a phone screen: wider range, taller rise, broad band */}
<VoiceScreen />
</VoiceBeam>
scale sizes the whole effect as one thing — lobes, spacing, range, bend, band, core, blur and flow all multiply by it — on top of the preset and any individual geometry prop:
<VoiceBeam type="pill" scale={1.3}>
The wrapper carries data-voice-type. voiceDefaults, voiceTypePresets and resolveVoiceDefaults(type) are exported so a host can read the numbers a preset resolves to.
Driving it yourself
Without a stream, the beam follows level (0–1). Pass a number when it changes occasionally, or a getter when it changes every frame — the getter is sampled by the driver without re-rendering your tree:
// From your own analyser, a playback meter, a speech API's volume event…
<VoiceBeam level={() => meter.current}>
<Card />
</VoiceBeam>
// Or a plain value
<VoiceBeam level={speaking ? 0.8 : 0}>
<Card />
</VoiceBeam>
Set idle={0} if you want nothing at all while silent; by default the beam keeps a soft breathing presence so it never looks dead.
Response
Four knobs shape how the glow answers the sound:
<VoiceBeam
sensitivity={2} // input gain — raise for quiet sources
threshold={0.03} // noise gate: below this is silence
attack={0.1} // seconds to rise
release={0.5} // seconds to settle
>
And four shape the glow itself:
<VoiceBeam
reach={1.4} // how tall it grows at full level
spread={0.6} // how far it widens; the side lobes drift outward too
flow={80} // px/s the spectrum travels sideways at full level
bands // low / mid / high bands move the lobes independently (default)
>
flow is what makes the colours move: while a voice is heard the lobes slide left to right (right to left for a negative value), wrapping around at the edges, so every colour takes a turn at the centre; as the voice stops the flow settles with it. flow={0} holds the spectrum still.
With bands on, the center lobe follows the lows, its neighbours the mids and the outer pair the highs, so a voice makes the colors ripple outward rather than one blob pumping. When driving with level, the bands are synthesised from it, so the lobes still dance.
Motion
motion moves the whole glow: how far its lobes are gathered into one compact beam, where that beam sits, the level it is held at while gathered. At rest (the default) the glow is the voice glow. Pass a getter for anything that moves — it is sampled every frame without re-rendering your tree:
<VoiceBeam motion={() => ({ gather: 1, offset: 0.4, heldLevel: 0.5 })}>
| Field | Range | What |
|---|---|---|
gather | 0–1 | 0 the voice glow, 1 the lobes gathered into one beam |
offset | × half the lobe ring | where the beam sits (0 = centre) |
stretch | 0–1 | extra width while it moves |
heldLevel | 0–1 | the level the glow is held at while gathered, so the beam has colour |
cornerFollow | 0–1 | how much the beam rides the element's corner arcs |
While gathered the distortion settles out in about a quarter second — once it is gone the wrapper carries data-voice-warp="off" and the warp layers leave the paint — and it eases back as the beam releases.
Processing — the beam sweeping the range while something is transcribing or thinking, eased into each turn and morphing in and out — is a Libraries Pro feature built on motion: the Pro skill adds a useVoiceProcessing(thinking) hook to your project.
Shape
bend is the gravity: as the voice rises, the ceiling the glow is masked to gains extra height at the centre — none at the ends — and a faint rim traces that curve, so the glow's top contour humps upward like space bending around the voice. It is in px at full level (default 60; 0 keeps the plain ellipse). rangeWidth sets how wide the hump is.
The band is the light along that contour — an organic bell, exp(-(|x| / spread)^curve), drawn on its own canvas layer and flattening onto the edge at both ends. It fades in with the bend and reacts to the voice on its own — its red and blue fringes split further from the core and it thickens as the level rises, and it turns with the glow's hue drift:
<VoiceBeam
bandStrength={1.55} // opacity; 0 hides it
bandWidth={2.15} // thickness
bandPosition={0.35} // peak height as a fraction of the glow's ceiling
bandCurve={1.75} // bell exponent: < 2 exponential / cusp-like, 2 gaussian, > 2 flat-topped
bandSpread={0.87} // bell width vs the half-range: small = spike with long tails, large = dome
bandSkew={0.12} // asymmetry; positive widens the right, steepens the left
bandOffset={-27} // px shift of the whole line; negative sinks it
bandTail={0.59} // the ends rise again in the corners (× the peak)
bandTailPosition={0.67} // the rise starts 67% of the way to the edge, full lift at the corner
bandTailCurve={2.4} // higher = a sharper hook
bandTailOverflow={15} // runs 15px past each side; the component crops the hook
bandAberration={0.89} // how far the red / blue fringes split, 0–1
>
Distortion warps the glow under the band sideways: a slowly drifting noise field displaces the inner light and the bloom horizontally (an SVG feDisplacementMap), stronger as the voice rises, so the colours shimmer and stretch like light through bent space. Only the glow under the band line warps — the layers are split at the line with a per-frame clip-path — and it is independent of bandStrength, so the line can be invisible and the warp strong. The edge stroke stays crisp. It is off while the glow is gathered (see Motion).
<VoiceBeam
distortion={0.62} // 0 turns the filter off
distortionDetail={2.3} // < 1 broad slow waves, > 1 finer ripples
>
Twelve multipliers (defaults are the tuned geometry) reshape the resting glow; the voice still drives the motion on top:
<VoiceBeam
bend={24} // px the top contour humps up at full level
glowWidth={1.2} // every lobe wider
glowHeight={0.8} // every lobe shorter
lobeSpacing={1.3} // lobes further apart (and a longer flow ring)
rangeWidth={1.5} // the visible ellipse wider
rangeHeight={1} // … and taller
softness={1.2} // softer lobe edges (below 1: crisper)
coreSize={1.4} // the white hot spot at the centre
strokeScale={1} // colours in the 1px edge stroke
innerScale={1} // the soft light inside the element
innerHeight={1.5} // … reaching further in
bloomScale={1} // the blurred halo
bloomHeight={1.3} // … climbing higher
>
Color variants
<VoiceBeam colorVariant="colorful" /> {/* Full spectrum (default) */}
<VoiceBeam colorVariant="mono" /> {/* Grayscale */}
<VoiceBeam colorVariant="ocean" /> {/* Blue-purple tones */}
<VoiceBeam colorVariant="sunset" /> {/* Orange-yellow-red tones */}
<VoiceBeam colorVariant="forest" /> {/* Green-teal tones */}
<VoiceBeam colorVariant="candy" /> {/* Pink-magenta tones */}
<VoiceBeam colorVariant="ice" /> {/* Cyan-pale blue tones */}
<VoiceBeam colorVariant="gold" /> {/* Amber-yellow tones */}
All variants except mono drift slowly through a hue cycle (hueRange, hueDuration); staticColors holds them still.
Your own colours, over any variant:
<VoiceBeam
colors={['#ffc915', '#7ec4ff', '#b428e6', '#eb64a0', '#ffb07a', '#9aa0ff', '#7fd9ee']} // 7 lobes: centre, then pairs outward
bandColors={{ core: '#c58bff', above: '#ff7ab6', mid: '#7ec4ff', below: '#2dffab' }} // the band's ridge and fringes
>
Slots you leave out keep the variant's colour for the theme. Light mode ships a candy palette (gold, sky, violet, rose, peach) with a violet-cored band; dark keeps the deeper spectrum and a white-cored band.
Theme
<VoiceBeam theme="dark" /> {/* Dark background (default) */}
<VoiceBeam theme="light" /> {/* Light background */}
<VoiceBeam theme="auto" /> {/* Follows prefers-color-scheme */}
Strength and play / pause
<VoiceBeam strength={0.7} active={listening} onDeactivate={() => console.log('faded out')}>
<Card />
</VoiceBeam>
strength scales the beam layers only, never the children. active={false} fades the beam out and stops the audio analysis.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Content to wrap |
type | 'default' | 'pill' | 'mobile' | 'default' | Host preset; sets the geometry defaults |
scale | number | 1 | Size of the whole effect — every px dimension at once |
stream | MediaStream | null | — | Audio to react to; wins over level |
level | number | () => number | 0 | Manual drive, 0–1 |
sensitivity | number | 3.1 | Input gain on the analysed audio |
threshold | number | 0.015 | Noise gate, 0–1 |
attack | number | 0.325 | Seconds to rise toward a louder level |
release | number | 0.86 | Seconds to settle after the sound drops |
idle | number | 0.23 | Resting presence while silent, 0–1 |
breatheDuration | number | 5.2 | Period of the idle breathing, seconds |
reach | number | 1.2 | Height gain at full level (1.8 on light; 1.35 for pill, 3 for mobile) |
spread | number | 1.05 | Width gain at full level (0.8 on light; 1.1 for pill, 0.45 for mobile) |
bands | boolean | true | Let the frequency bands move the lobes independently |
flow | number | 48 | Sideways travel of the spectrum, px/s at full level; negative reverses, 0 holds |
motion | VoiceBeamMotion | () => VoiceBeamMotion | rest | The glow gathered into a beam and where it sits (see Motion); Processing is Pro |
colorVariant | 'colorful' | 'mono' | 'ocean' | 'sunset' | 'forest' | 'candy' | 'ice' | 'gold' | 'colorful' | Color palette |
colors | string[] | — | Up to 7 lobe colours overriding the palette |
bandColors | { core?, above?, mid?, below? } | — | The band's ridge and fringe colours |
theme | 'dark' | 'light' | 'auto' | 'dark' | Background adaptation |
staticColors | boolean | false | Disable the hue drift |
hueRange | number | 24 / 40 | Hue drift range in degrees (dark / light) |
hueDuration | number | 12 / 8.5 | Hue drift period in seconds (dark / light) |
active | boolean | true | Whether the effect is on |
paused | boolean | false | Freezes the effect in place (glow, band and analysis hold their last frame) without fading it out |
borderRadius | number | auto-detected | Custom border radius in px |
brightness | number | 1.15 / 0.95 | Glow brightness multiplier (dark / light; the dark theme itself is 1.1, pill runs 1.35, mobile 1.2 on dark) |
saturation | number | 1.2 / 1.6 | Glow saturation multiplier (dark / light; pill and mobile run 1.5 on dark) |
glowSize | number | 1 | Multiplies the bloom blur radius |
strokeOpacity / innerOpacity / bloomOpacity | number | 1 | Per-layer opacity multipliers on the theme's own (same as the --voice-*-opacity CSS hooks) |
bend | number | 60 | Px the glow's top contour humps up at the centre at full level; 0 is the plain ellipse |
bandStrength | number | 1.55 (1.8 for mobile; on light 1.7, or 2 for pill) | Opacity of the band along the contour; 0 hides it |
bandWidth | number | 2.15 | Thickness of the band |
bandPosition | number | 0.35 | Peak height as a fraction of the glow's ceiling |
bandCurve | number | 1.75 | Bell exponent; < 2 exponential, 2 gaussian, > 2 flat-topped |
bandSpread | number | 0.87 | Bell width vs the half-range |
bandSkew | number | 0.12 | Asymmetry, −0.6–0.6 |
bandOffset | number | -27 | Px shift of the whole band line |
bandTail | number | 0.59 | Rise of the band's ends in the corners, × the peak; 0 for pill |
bandTailPosition | number | 0.67 | Where the rise starts, × the centre-to-edge distance; full lift at the corner |
bandTailCurve | number | 2.4 | Exponent of the rise; higher = a sharper hook at the corner |
bandTailOverflow | number | 15 | Px the band runs past each side, so the hook is cropped by the component |
bandAberration | number | 0.89 | Chromatic split of the band's fringes, 0–1 |
distortion (off on WebKit / Safari for large hosts, see notes) | number | 0.62 | Horizontal warp of the glow under the band, 0–1; 0 removes the filter |
distortionDetail | number | 2.3 | Grain of the distortion noise |
glowWidth / glowHeight | number | 0.65 / 1.25 | Width / height of every lobe, all layers |
lobeSpacing | number | 0.85 | Distance between lobes, and the flow ring |
rangeWidth / rangeHeight | number | 0.75 / 1 | The visible ellipse the glow is masked to |
softness | number | 1.07 | Lobe edge fade: below 1 crisper, above 1 softer |
coreSize | number | 1 | The white hot spot at the centre |
coreLight | number | 0 | The epicentre: a white wash under the band line so the source reads lighter than the band, 0–3 (1.8 on light; past 1 the solid core widens and the wash grows) |
coreLightWidth / coreLightHeight | number | 1 | Size of the epicentre wash |
strokeScale | number | 1 | Size of the colours in the edge stroke |
innerScale / innerHeight | number | 1 | Size and reach of the inner light |
bloomScale / bloomHeight | number | 1 | Size and climb of the blurred halo |
strength | number | 1 / 0.8 | Effect opacity (0–1), beam layers only (dark / light; mobile is 1 on both) |
className | string | — | Additional class on the wrapper |
style | CSSProperties | — | Additional inline styles on the wrapper |
css | string | — | Extra CSS appended after the generated stylesheet; write {id} for the instance id |
onLevel | (level: number) => void | — | Called every frame with the smoothed level |
onActivate | () => void | — | Called when the fade-in completes |
onDeactivate | () => void | — | Called when the fade-out completes |
All standard HTMLDivElement attributes are also forwarded to the wrapper. The wrapper carries data-listening while a stream is attached, for styling the host.
CSS hooks
The generated stylesheet reads a few custom properties with a fallback of 1, so a host can retune a layer without touching the props:
| Variable | Layer |
|---|---|
--voice-stroke-opacity | The colored 1px edge stroke |
--voice-inner-opacity | The soft light inside the element |
--voice-bloom-opacity | The blurred halo |
--voice-band-opacity | The band along the bend |
--voice-hue-base | A fixed hue shift added to the drift (deg) |
The driver also writes --vb-level-{id} (the smoothed 0–1 level) on the wrapper each frame, for anything else in the host that wants to follow the voice.
The wrapper carries data-voice-type, so the host can style its own controls per type.
useMicrophone
const { stream, state, error, supported, start, stop } = useMicrophone({
constraints: { deviceId: '…' }, // extra getUserMedia audio constraints
autoStart: false,
});
state is one of idle, requesting, live, denied, unsupported, error. By default the hook turns the browser's echo cancellation, noise suppression and auto gain off, so the beam sees the real dynamics of the voice; pass constraints: {} to keep the browser defaults. The stream is stopped on unmount.
The hook is a convenience; any MediaStream with an audio track works, including a remote WebRTC peer's — a voice call UI can light up with the other side's voice.
How it works
VoiceBeam renders a wrapper <div> with three bottom-centered layers, clipped to the element:
::after— the stroke: the palette painted into the 1px edge ring, with a hot white core at the center::before— the inner glow: the same lobes as soft light inside the element, faded off at the corners[data-voice-beam-bloom]— the bloom: a blurred halo above the content[data-voice-beam-warp="inner"]/[data-voice-beam-warp="bloom"]— withdistortionon, mirrors of the two soft layers clipped to below the band line, carrying the displacement filter[data-voice-beam-band]— a canvas the driver draws the band on, above everything
Every lobe's size and position multiplies a per-instance custom property. A single shared requestAnimationFrame loop (capped at ~60 fps) reads the audio each frame — RMS level plus low / mid / high band energy from one AnalyserNode per instance — shapes it (gain, gate, soft saturation), follows it with an attack/release envelope, advances the flow, folds in the idle breathing and the hue drift, and writes the properties. The browser does the painting; the loop is a few arithmetic ops per instance.
The Web Audio graph is one shared AudioContext, one source node per stream (reference-counted, so several beams can share a microphone), and nothing connected to the output — the audio is analysed, never played. The loop pauses while the instance is inactive or scrolled offscreen, and under prefers-reduced-motion the breathing, flow and hue drift stop while the reaction to sound stays, since that is a meter rather than decoration.
Project structure
voice-glow/
├── src/
│ ├── index.ts # Public exports
│ ├── VoiceBeam.tsx # React component
│ ├── types.ts # TypeScript type definitions
│ ├── styles.ts # CSS generation engine, palettes and lobe geometry
│ ├── voiceDriver.ts # Shared rAF loop: analysis, envelope, custom properties
│ ├── audio.ts # Shared AudioContext and analyser leases
│ └── useMicrophone.ts # getUserMedia hook
├── dist/ # Built output (ESM + CJS + types)
├── package.json
├── LICENSE
└── README.md
Requirements
- React 18+
- Web Audio API and
getUserMediafor the microphone path (every modern browser; a secure context —httpsorlocalhost— is required for the microphone) - CSS
@propertyfor the interpolated fade in / out (Chrome 85+, Safari 15.4+, Firefox 128+); without it the fade steps rather than tweens - Phones: the soft layers (inner light, bloom, their warp mirrors, the epicentre) are rastered at half resolution and scaled back up by the compositor — the same picture for blurred gradients at a quarter of the raster and filter work — the band canvas caps its backing store at 2×, and when a device still cannot hold 60 the driver updates every other frame (the glow's dynamics are far slower than 30 Hz) and probes full rate again every few seconds. The halving is explicit (a per-layer factor on every length), so it runs on every engine.
- WebKit / Safari notes: the
distortionwarp is off there on hosts larger than about 60,000 px² (a chat input keeps it, a phone screen or its crop does not) (Safari evaluates SVG filters on HTML content on the CPU every paint, which on a phone-sized host costs an order of magnitude in frame rate; a chat input or pill keeps it), and the band's blur runs in CSS on two canvases instead of the 2D context'sfilter, which Safari lacks. The look otherwise matches Chromium.
Accessibility
The effect layers are purely decorative and use pointer-events: none. They do not affect keyboard navigation or screen readers. Under prefers-reduced-motion: reduce the idle breathing, the flow and the hue drift are disabled. The microphone is only ever requested by your own call to start().
License
Source-backed parameter reference
Implementation signatures and types are generated from the pinned local source. Optional props, native inherited types, union branches and never exclusions are preserved. A forwardRef implementation's ref is a React reference, not an additional business prop.
VoiceBeam({
children,
type = 'default',
look: lookProp = 'glow',
dotSize = 1,
dotGap = 1,
dotShape = 'round',
lineWidth = 1,
lineGap = 1,
linePattern = 'rows',
seeThrough = false,
texture = 0.6,
gravity = 1,
surfaceHeight = 1,
surfaceCurve = 1,
surfaceTail = 0,
surfaceTailPosition = 0.6,
surfaceTailCurve = 2.4,
surfaceFade = 0.2,
scale: scaleProp,
stream = null,
level = 0,
sensitivity = 3.1,
threshold = 0.015,
attack = 0.325,
release = 0.86,
idle: idleProp,
breatheDuration = 5.2,
reach: reachProp,
spread: spreadProp,
bands = true,
flow: flowProp,
motion = null,
colorVariant = 'colorful',
colors,
bandColors,
theme = 'dark',
staticColors = false,
hueRange: hueRangeProp,
hueDuration: hueDurationProp,
active = true,
paused = false,
borderRadius: customBorderRadius,
brightness: brightnessProp,
saturation: saturationProp,
glowSize: glowSizeProp,
strokeOpacity: strokeOpacityProp,
innerOpacity: innerOpacityProp,
bloomOpacity: bloomOpacityProp,
bend: bendProp,
bandStrength: bandStrengthProp,
bandWidth: bandWidthProp,
bandPosition: bandPositionProp,
bandCurve: bandCurveProp,
bandSpread: bandSpreadProp,
bandSkew: bandSkewProp,
bandOffset: bandOffsetProp,
bandTail: bandTailProp,
bandTailPosition: bandTailPositionProp,
bandTailCurve: bandTailCurveProp,
bandTailOverflow: bandTailOverflowProp,
bandAberration: bandAberrationProp,
distortion: distortionProp,
distortionDetail: distortionDetailProp,
glowWidth: glowWidthProp,
glowHeight: glowHeightProp,
lobeSpacing: lobeSpacingProp,
rangeWidth: rangeWidthProp,
rangeHeight: rangeHeightProp,
softness: softnessProp,
coreSize: coreSizeProp,
coreLight: coreLightProp,
coreLightWidth: coreLightWidthProp,
coreLightHeight: coreLightHeightProp,
strokeScale: strokeScaleProp,
innerScale: innerScaleProp,
innerHeight: innerHeightProp,
bloomScale: bloomScaleProp,
bloomHeight: bloomHeightProp,
strength: strengthProp,
className,
style,
css: extraCss,
onLevel,
onActivate,
onDeactivate,
onAnimationEnd: consumerOnAnimationEnd,
...props
}: VoiceBeamProps, ref: ForwardedRef<HTMLDivElement>)
export type VoiceBeamType = 'default' | 'pill' | 'mobile';
export type VoiceBeamTheme = 'dark' | 'light' | 'auto';
export type VoiceBeamLook = 'glow' | 'dots' | 'lines';
export type VoiceBeamDotShape = 'round' | 'square';
export type VoiceBeamLinePattern = 'rows' | 'columns' | 'grid';
export type VoiceBeamColorVariant =
| 'colorful'
| 'mono'
| 'ocean'
| 'sunset'
| 'forest'
| 'candy'
| 'ice'
| 'gold';
export type VoiceBeamLevel = number | (() => number);
export interface VoiceBeamMotion {
gather: number;
offset?: number;
stretch?: number;
heldLevel?: number;
cornerFollow?: number;
}
export interface VoiceBeamProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
/** Content to wrap with the voice beam effect */
children: ReactNode;
/**
* Host preset: 'default' (chat input), 'pill' (recording pill) or
* 'mobile' (bottom of a phone screen). Sets the defaults of the
* geometry props below for that host; any prop you pass still wins.
* @default 'default'
*/
type?: VoiceBeamType;
/**
* How the voice is drawn: coloured light, or a surface of dots or lines
* that the voice raises and gravity brings down.
* @default 'glow'
*/
look?: VoiceBeamLook;
/**
* Dot radius, as a multiplier on the tuned size. `look="dots"` only.
* @default 1
*/
dotSize?: number;
/**
* Spacing between dots, as a multiplier — below 1 is a denser, finer
* surface, above 1 a sparser, coarser one. `look="dots"` only.
* @default 1
*/
dotGap?: number;
/**
* The dots' shape: round, or square for a pixel grid. `look="dots"` only.
* @default 'round'
*/
dotShape?: VoiceBeamDotShape;
/**
* Line width, as a multiplier on the tuned width (lines thin out into the
* distance). `look="lines"` only.
* @default 1
*/
lineWidth?: number;
/**
* Spacing between lines, as a multiplier — the rows and the columns alike.
* `look="lines"` only.
* @default 1
*/
lineGap?: number;
/**
* Which way the lines run: across the surface, into the depth, or both.
* `look="lines"` only.
* @default 'rows'
*/
linePattern?: VoiceBeamLinePattern;
/**
* Draw the lines as a wireframe: those behind a raised ridge show through
* it instead of being hidden by it. `look="lines"` only.
* @default false
*/
seeThrough?: boolean;
/**
* Organic texture, 0–1: a slow ripple the flow carries across the
* surface. 0 is a still sheet that only the voice moves. Dots and lines.
* @default 0.6
*/
texture?: number;
/**
* How hard the surface falls when the voice drops, as a multiplier on its
* gravity. Lower floats down, higher drops like sand. Dots and lines.
* @default 1
*/
gravity?: number;
/**
* How tall the surface stands along the bottom, as a multiplier — how far
* back the sheet runs, and so how much of the host it covers. Dots and lines.
* @default 1
*/
surfaceHeight?: number;
/**
* How far the surface arcs, as a multiplier: its sides and far edge fall
* away like a horizon. 0 is a flat sheet; below 0 it cups upward instead.
* Dots and lines.
* @default 1
*/
surfaceCurve?: number;
/**
* The surface's tails: how far its ends rise toward the corners, as a share
* of the sheet's height — the ends curl up into the host's corners instead
* of running flat into its sides. Below 0 they drop away instead. Dots and lines.
* @default 0
*/
surfaceTail?: number;
/**
* Where the tails start, as a share of the distance from the centre to
* the side (0.8 = only the last 20%); they reach their full lift at the edge.
* @default 0.6
*/
surfaceTailPosition?: number;
/**
* Exponent of the tails' rise: 1 a straight ramp, 2 a parabola, higher a
* hook that stays low and whips up at the edge.
* @default 2.4
*/
surfaceTailCurve?: number;
/**
* How far in from the sides the surface dissolves, as a share of its
* half-width, 0–1. 0 runs it hard into the edges; 1 fades it from the
* centre out. Dots and lines.
* @default 0.2
*/
surfaceFade?: number;
/**
* Size of the whole effect. Multiplies every pixel dimension at once —
* lobes, their spacing, the visible range, the bend, the band, the core,
* the bloom blur, the flow speed — so the glow grows or shrinks as one
* thing, on top of the type preset and the individual geometry props.
* @default 1 (0.45 for `type="pill"`)
*/
scale?: number;
/**
* Audio to react to. When set, the beam analyses this stream (level plus
* low / mid / high bands) and ignores `level`. Get one from the
* `useMicrophone` hook or any `getUserMedia` / WebRTC source.
*/
stream?: MediaStream | null;
/**
* Manual drive, 0–1, used when no `stream` is given. A number re-renders
* on change; a getter is sampled once per frame without re-rendering.
* @default 0
*/
level?: VoiceBeamLevel;
/**
* Input gain on the analysed audio. The default suits a laptop microphone
* at conversational distance; raise it for quiet sources, lower it for a
* close or hot one.
* @default 3.1
*/
sensitivity?: number;
/**
* Noise gate, 0–1. Levels below it read as silence, so room hum and
* fan noise do not keep the beam twitching.
* @default 0.015
*/
threshold?: number;
/**
* Seconds the glow takes to rise toward a louder level.
* @default 0.325
*/
attack?: number;
/**
* Seconds the glow takes to settle after the sound drops.
* @default 0.86
*/
release?: number;
/**
* Resting presence, 0–1 — how much the beam breathes while silent so it
* never looks dead. 0 hides it completely between sounds.
* @default 0.23
*/
idle?: number;
/**
* Period of the idle breathing in seconds.
* @default 5.2
*/
breatheDuration?: number;
/**
* How tall the glow grows at full level, as a multiple of its resting
* height. The rise is centered on the bottom edge.
* @default 1.2 (1.8 on the light theme; 1.35 for `pill`, 3 for `mobile`)
*/
reach?: number;
/**
* How far the glow widens at full level, as a fraction of its resting
* width. Side lobes drift outward as well, so a loud sound spreads.
* @default 1.05 (0.8 on the light theme; 1.1 for `pill`, 0.45 for `mobile`)
*/
spread?: number;
/**
* Let the low / mid / high bands move the colored lobes independently
* (center follows the lows, the sides the mids and highs). With a
* manual `level` the bands are synthesised from the level, so the lobes
* still dance. Off, every lobe follows the one level.
* @default true
*/
bands?: boolean;
/**
* How fast the spectrum travels sideways, in px per second at full
* level — the lobes slide left to right while a voice is heard and rest
* when it stops, so every colour takes a turn at the centre. Negative
* flows right to left; 0 holds the lobes in place.
* @default 48
*/
flow?: number;
/**
* The glow's large-scale motion, sampled every frame: how far the lobes
* are gathered into one compact beam, where it sits, the level it is held
* at. At rest (the default) the glow is the voice glow. Pass a getter for
* anything that moves; the Pro processing state is one.
*/
motion?: VoiceBeamMotion | (() => VoiceBeamMotion | null | undefined) | null;
/**
* Color variant for the beam
* @default 'colorful'
*/
colorVariant?: VoiceBeamColorVariant;
/**
* Your own lobe colours, centre first then the pairs outward — up to
* seven, any CSS hex or rgb. Slots you leave out (or that fail to
* parse) keep the variant's colour for the theme.
*/
colors?: string[];
/**
* The band's colours: the core ridge and its chromatic fringes above,
* between and below. Any CSS hex or rgb; unset ones keep the theme's
* defaults (white core with red / green / blue fringes on dark, a pink
* core with rose / lavender / sky on light).
*/
bandColors?: { core?: string; above?: string; mid?: string; below?: string };
/**
* Theme mode — adapts beam colors for dark or light backgrounds;
* 'auto' follows prefers-color-scheme
* @default 'dark'
*/
theme?: VoiceBeamTheme;
/**
* Disable the slow hue drift for static colors
* @default false
*/
staticColors?: boolean;
/**
* Hue drift range in degrees (the palette wanders ± this far)
* @default 24 (dark) / 40 (light)
*/
hueRange?: number;
/**
* Period of the hue drift in seconds
* @default 12 (dark) / 8.5 (light)
*/
hueDuration?: number;
/**
* Whether the effect is on. Off fades the beam out and stops the audio
* analysis.
* @default true
*/
active?: boolean;
/**
* Freezes the effect where it is — the glow, the band and the audio
* analysis all hold their last frame — without fading it out. Other
* props still shape the held frame, and the clocks resume where they
* stopped.
* @default false
*/
paused?: boolean;
/**
* Custom border radius in pixels. When omitted, the component
* auto-detects the border-radius of the first child element.
*/
borderRadius?: number;
/**
* Brightness multiplier for the glow
* @default 1.15 (dark chat input) / 0.95 (light); the dark theme itself is 1.1, `pill` runs 1.35, `mobile` 1.2 on dark
*/
brightness?: number;
/**
* Saturation multiplier for the glow
* @default 1.2 (dark) / 1.6 (light); `pill` and `mobile` run 1.5 on dark
*/
saturation?: number;
/**
* Multiplies the bloom blur radius, so the halo reads tighter (< 1) or
* wider and softer (> 1).
* @default 1
*/
glowSize?: number;
/**
* Per-layer opacity multipliers on the theme's own: the crisp edge
* stroke, the soft inner light and the blurred bloom. The same knobs
* as the `--voice-stroke-opacity` / `--voice-inner-opacity` /
* `--voice-bloom-opacity` CSS hooks.
* @default 1
*/
strokeOpacity?: number;
innerOpacity?: number;
bloomOpacity?: number;
/* ── Shape ──────────────────────────────────────────────────────────
* Each multiplier below defaults to the tuned geometry. They scale
* the resting sizes; the voice still drives the motion on top. */
/**
* Bend — how much, in px, the glow's top contour humps up at the centre
* at full level. The ceiling the glow is masked to gains that height in
* the middle and none at the ends, and a faint rim traces the curve, so
* the glow's outline bows upward with the voice like space bending
* around it. 0 keeps the contour at its plain ellipse.
* @default 60
*/
bend?: number;
/* ── Band ──────────────────────────────────────────────────────────
* The light along the bend's contour — an organic bell drawn on its own
* layer, exp(-(|x| / spread)^curve), flattening onto the edge at both
* ends. It fades in with the bend, and reacts to the voice on its own:
* the fringes split further and the band thickens as the level rises,
* and it turns with the hue drift. */
/** Opacity of the band, 0 hides it. @default 1.55 (1.8 for `mobile`; on light 1.7, or 2 for `pill`) */
bandStrength?: number;
/** Thickness of the band. @default 2.15 */
bandWidth?: number;
/** Height of the band's peak as a fraction of the glow's ceiling, 0.1–1.3. @default 0.35 */
bandPosition?: number;
/** Bell exponent: below 2 an exponential, cusp-like rise; 2 a gaussian; above, a flatter top. @default 1.75 */
bandCurve?: number;
/** Bell width as a fraction of the glow's half-range: small is a narrow spike with long tails, large a broad dome. @default 0.87 */
bandSpread?: number;
/** Asymmetry, −0.6–0.6: positive widens the right side and steepens the left. @default 0.12 */
bandSkew?: number;
/** Vertical shift of the whole band line in px; negative sinks it toward the edge. @default -27 */
bandOffset?: number;
/** How far the band's ends rise again toward the corners, as a fraction of its peak, 0–1 — the line opens upward at the edges instead of dying flat. @default 0.59 (0 for `pill`) */
bandTail?: number;
/** Where the tail's rise starts, as a share of the distance from the beam's centre to the element's edge (0.85 = only the last 15%); it always reaches the full lift exactly at the corner. @default 0.67 */
bandTailPosition?: number;
/** Exponent of the rise: 1 a straight ramp, 2 a parabola, higher a hook that stays low and whips up at the corner. @default 2.4 */
bandTailCurve?: number;
/** Px the band runs past each side of the element, so the hook peaks outside and is cropped by the component's edge. Scales with `scale`. @default 15 */
bandTailOverflow?: number;
/** Chromatic aberration — how far the red and blue fringes split from the core, 0–1. @default 0.89 */
bandAberration?: number;
/**
* Distortion — how much the glow under the band warps sideways, 0–1.
* A slowly drifting noise field displaces the inner light and the bloom
* horizontally, stronger as the voice rises, so the colours shimmer and
* stretch like light through bent space. Only the glow UNDER the band
* line warps — the layers are split at the line — and it is independent
* of `bandStrength`, so the line can be invisible and the warp strong.
* The edge stroke stays crisp. 0 turns the filter off entirely.
* @default 0.62
*/
distortion?: number;
/** Grain of the distortion's noise: below 1 broad slow waves, above 1 finer ripples. @default 2.3 */
distortionDetail?: number;
/** Width of every colour lobe, all layers — on the dots and lines, the width of each hill. @default 0.65 */
glowWidth?: number;
/** Height of every colour lobe, all layers. @default 1.25 */
glowHeight?: number;
/** Distance between the lobes (and the ring the flow travels) — on the dots and lines, between the hills. @default 0.85 */
lobeSpacing?: number;
/** Width of the visible ellipse the glow is masked to. @default 0.75 */
rangeWidth?: number;
/** Height of the visible ellipse the glow is masked to. @default 1 */
rangeHeight?: number;
/** Where each lobe fades out — below 1 crisper, above 1 softer. @default 1.07 */
softness?: number;
/** The white hot spot at the centre of the edge. @default 1 */
coreSize?: number;
/**
* The epicentre: a soft white wash under the band line at the centre of
* the edge, so the source reads lighter than the band. 0–3: up to 1 the
* wash's opacity, past 1 its solid white core widens and the whole wash
* grows, washing out more of the colour beneath.
* @default 0 (1.8 on the light theme)
*/
coreLight?: number;
/** Width of the epicentre wash. @default 1 */
coreLightWidth?: number;
/** Height of the epicentre wash. @default 1 */
coreLightHeight?: number;
/** Size of the colours painted into the 1px edge stroke. @default 1 */
strokeScale?: number;
/** Size of the soft light inside the element. @default 1 */
innerScale?: number;
/** Extra height of the inner light — how far it reaches into the element. @default 1 */
innerHeight?: number;
/** Size of the blurred halo. @default 1 */
bloomScale?: number;
/** Extra height of the halo — how far the soft light climbs. @default 1 */
bloomHeight?: number;
/**
* Overall strength/opacity of the effect (0–1).
* Only affects the beam layers — not the children.
* @default 1 (dark) / 0.8 (light); 1 on both for `type="mobile"`
*/
strength?: number;
/**
* Additional class name for the container
*/
className?: string;
/**
* Additional inline styles for the container
*/
style?: CSSProperties;
/**
* Extra CSS appended after the beam's own generated stylesheet. Write
* `{id}` wherever the instance id belongs — the root is
* `[data-voice-beam="{id}"]`, its layers the `::before` / `::after`
* pseudo-elements and `[data-voice-beam-bloom]`, and keyframes are named
* `*-{id}` — and it is substituted per instance.
*/
css?: string;
/**
* Called every frame with the smoothed level (0–1) the beam is showing.
* Handy for a meter or a "listening" label; avoid setting React state
* from it on every call.
*/
onLevel?: (level: number) => void;
/**
* Callback when the fade-in completes
*/
onActivate?: () => void;
/**
* Callback when the fade-out completes
*/
onDeactivate?: () => void;
}
Destructuring defaults in source
Only defaults written directly in parameter/props destructuring are listed. Conditional expressions, runtime theme and computed values require source inspection; this is not an exhaustive behavior-default list.
// VoiceBeam
type = 'default'
// VoiceBeam
look = 'glow'
// VoiceBeam
dotSize = 1
// VoiceBeam
dotGap = 1
// VoiceBeam
dotShape = 'round'
// VoiceBeam
lineWidth = 1
// VoiceBeam
lineGap = 1
// VoiceBeam
linePattern = 'rows'
// VoiceBeam
seeThrough = false
// VoiceBeam
texture = 0.6
// VoiceBeam
gravity = 1
// VoiceBeam
surfaceHeight = 1
// VoiceBeam
surfaceCurve = 1
// VoiceBeam
surfaceTail = 0
// VoiceBeam
surfaceTailPosition = 0.6
// VoiceBeam
surfaceTailCurve = 2.4
// VoiceBeam
surfaceFade = 0.2
// VoiceBeam
stream = null
// VoiceBeam
level = 0
// VoiceBeam
sensitivity = 3.1
// VoiceBeam
threshold = 0.015
// VoiceBeam
attack = 0.325
// VoiceBeam
release = 0.86
// VoiceBeam
breatheDuration = 5.2
// VoiceBeam
bands = true
// VoiceBeam
motion = null
// VoiceBeam
colorVariant = 'colorful'
// VoiceBeam
theme = 'dark'
// VoiceBeam
staticColors = false
// VoiceBeam
active = true
// VoiceBeam
paused = false
Native and external types
External types remain defined by React or native dependencies, not manually expanded into guessed props. The installed version's TypeScript definitions are authoritative.
HTMLDivElement
Omit<HTMLAttributes<HTMLDivElement>, 'children'>
HTMLAttributes<HTMLDivElement>
ReactNode
MediaStream
CSSProperties
ForwardedRef<HTMLDivElement>