PushToTalkButton

The round microphone button of the voice HUD. It toggles the mic and, while active, shows two pulsing rings and a stronger glow so the user can see that JARVIS is listening.

import { PushToTalkButton } from 'jarvis-react-ui';

Toggle the microphone

The button is fully controlled: keep the mic state yourself, pass it as active and flip it in onClick. onClick is a normal button click handler and receives the click event; a handler without arguments such as () => setLive((v) => !v) works too. A dashed rim rotates slowly in both states.

import { useState } from 'react';
import { PushToTalkButton } from 'jarvis-react-ui';

export default function Toggle() {
    const [live, setLive] = useState(false);

    return (
        <div className="flex flex-col items-center gap-4">
            <PushToTalkButton active={live} onClick={() => setLive((v) => !v)} />
            <span className="text-[10px] uppercase tracking-[1px] text-text-muted">
                Mic {live ? 'live' : 'off'}
            </span>
        </div>
    );
}

Hold to talk

All native <button> attributes and events are forwarded, including disabled, id, ref and the pointer and keyboard events. For walkie-talkie style input, turn the mic on in onPointerDown and off in onPointerUp; also stop on onPointerLeave and onPointerCancel so the mic does not stay live when the pointer slides off or the browser cancels the gesture. onKeyDown/onKeyUp for Space give keyboard users the same hold behavior (ignore e.repeat while the key is held).

import { useState } from 'react';
import { PushToTalkButton } from 'jarvis-react-ui';

export default function HoldToTalk() {
    const [live, setLive] = useState(false);
    const start = () => setLive(true);
    const stop = () => setLive(false);

    return (
        <div className="flex flex-col items-center gap-4">
            <PushToTalkButton
                active={live}
                aria-label="Hold to talk"
                onPointerDown={start}
                onPointerUp={stop}
                onPointerLeave={stop}
                onPointerCancel={stop}
                onKeyDown={(e) => {
                    if (e.key === ' ' && !e.repeat) start();
                }}
                onKeyUp={(e) => {
                    if (e.key === ' ') stop();
                }}
            />
            <span className="text-[10px] uppercase tracking-[1px] text-text-muted">
                {live ? 'Listening · release to send' : 'Hold to talk'}
            </span>
        </div>
    );
}

Custom content

children replace the default Mic icon. Mark the icon aria-hidden and describe the action with aria-label, because the button has no visible text.

import { useState } from 'react';
import { Radio } from 'lucide-react';
import { PushToTalkButton } from 'jarvis-react-ui';

export default function CustomContent() {
    const [open, setOpen] = useState(false);

    return (
        <PushToTalkButton
            active={open}
            onClick={() => setOpen((v) => !v)}
            aria-label="Open radio channel"
        >
            <Radio size={24} strokeWidth={1.8} aria-hidden="true" />
        </PushToTalkButton>
    );
}

Warning: ariaLabel is deprecated

The ariaLabel prop still works but is deprecated. Use the standard aria-label attribute instead; when both are set, aria-label wins.

With the orb state

In an assistant UI the button usually drives the orb state (AppOrbState) rather than a plain boolean. Here a click moves idle to listening, a second click ends the turn (thinking), and the reply plays out as speaking before returning to idle. A WaveformMeter on each side runs while audio flows, and a StatusLabel names the state.

import { useEffect, useState } from 'react';
import { PushToTalkButton, StatusLabel, WaveformMeter } from 'jarvis-react-ui';
import type { AppOrbState } from 'jarvis-react-ui';

/** States that advance on their own: [next state, delay in ms]. */
const AUTO_NEXT: Partial<Record<AppOrbState, [AppOrbState, number]>> = {
    thinking: ['speaking', 1200],
    speaking: ['idle', 2400],
};

export default function VoiceLoop() {
    const [state, setState] = useState<AppOrbState>('idle');

    useEffect(() => {
        const next = AUTO_NEXT[state];
        if (next === undefined) return;
        const id = setTimeout(() => setState(next[0]), next[1]);
        return () => clearTimeout(id);
    }, [state]);

    function handleClick() {
        setState((s) => (s === 'idle' ? 'listening' : s === 'listening' ? 'thinking' : s));
    }

    const audible = state === 'listening' || state === 'speaking';

    return (
        <div className="flex flex-col items-center gap-5">
            <div className="flex items-center gap-6">
                <WaveformMeter active={audible} />
                <PushToTalkButton active={state === 'listening'} onClick={handleClick} />
                <WaveformMeter active={audible} mirrored />
            </div>
            <StatusLabel state={state} />
        </div>
    );
}

For a global keyboard shortcut (for example Space anywhere on the page), listen for keydown/keyup in your app and update active yourself; a Hint can advertise the key.

Sound

Hovering plays hover_button and clicking plays click through the nearest SfxProvider. Without a provider the button stays silent. Pointer and key handlers you pass do not play extra sounds. See Sound effects.

Accessibility

  • Renders a native <button type="button">, so it is focusable and Enter and Space activate it. disabled disables it and dims it.
  • aria-label (default Push to talk) is the accessible name; the icon is decorative. The deprecated ariaLabel prop is used only when aria-label is not set.
  • aria-pressed mirrors active, so screen readers announce it as a toggle button that is pressed while the mic is live. Pass aria-pressed yourself to override it.
  • The focus ring (2px accent outline) is visible on keyboard focus only (focus-visible).

API

Prop Type Default Description
active boolean false Whether the microphone is live; shows the pulse rings and a stronger glow, and sets aria-pressed.
aria-label string 'Push to talk' Accessible name of the button, rendered as aria-label. Wins over the deprecated ariaLabel.
ariaLabel string Accessible name of the button. @deprecated Use the standard aria-label attribute instead; aria-label wins when both are set.
children ReactNode Content of the button; replaces the default microphone icon when set.
className string Extra classes for the root <button> element.
onClick MouseEventHandler<HTMLButtonElement> Called when the button is clicked, after the click sound plays. Handlers that take no arguments (() => void) work as well.

Also accepts inherited props from ButtonHTMLAttributes (10), HTMLAttributes (54), AriaAttributes (52), DOMAttributes (168), RefAttributes (1), Attributes (1).