Skip to content
Sign in
VoiceMic permission gateMenu

Mic permission gatePro

Asks for the microphone before the browser does, points at the prompt while it is up, and when the answer is no shows the exact steps for the user's browser or system. It opens by itself once the site setting flips to Allow.

ProThe ask, the browser prompt, a block with the steps for the browser, and the gate opening by itself once the site setting flips.

Installation

$ pnpm dlx shadcn@latest add @chatcn-pro/mic-permission-gate

Usage

tsx
import { MicPermissionGate } from "@/components/ui/chatcn/pro/mic-permission-gate"

<MicPermissionGate onSkip={() => setMode("text")}>
  <VoiceSession {...voice} />
</MicPermissionGate>

Leave status out and the gate runs itself: it reads the site permission, asks on Allow, closes the probe stream at once, and maps the answer to a state. It follows the Permissions API change event, so a user who unblocks the microphone from the address bar sees the gate open without a reload, and it retries by itself when a microphone is plugged in. Pass status and onRequest to drive it from your own call setup.

Recovery steps are written per browser (Chrome, Edge, Safari, Firefox and a generic fallback) and per platform, including phones, with the menu path shown as breadcrumbs. When the system is the one blocking (the browser allowed it but the operating system did not), the steps point at the system settings instead. The browser is detected from the user agent, and a small select switches it for people helping someone else.

Examples

Every state

ProEvery state of the card and the inline bar: blocked per browser, blocked by the system, no microphone, busy, insecure and allowed.

Inline above a composer

ProThe compact bar with the recovery steps open in place, above a composer.

Props

PropTypeNotes
status"prompt" | "requesting" | "granted" | "denied" | "system-blocked" | "not-found" | "in-use" | "unsupported"Controlled state. Leave undefined to let the gate run itself
onStatusChange(status) => voidEvery change, including the live permission flip
onRequest() => Promise<status | undefined> | status | undefinedYour own request. Default opens and closes a probe stream
variant"card" | "inline"Default card
title, descriptionReactNodeThe ask: what the microphone is for
privacyNoteReactNodeQuiet line under the ask. null hides it
allowLabelstringDefault "Allow microphone"
onSkip, skipLabel() => void, stringA way forward without voice. Default "Type instead"
browser"chrome" | "edge" | "safari" | "firefox" | "other"Override detection
platform"mac" | "windows" | "linux" | "ios" | "android" | "other"Override detection
defaultStepsOpenbooleanInline only: start with the steps open
childrenReactNodeRendered once allowed. Without it, a short confirmation

Keyboard

KeyAction
TabMoves through Allow, Type instead, the browser select and Try again
Enter, SpaceActivates the focused button, opens the browser select
Arrow keysMove through browsers in the open select

Accessibility

  • The card is a labelled section; each state change is announced once through a polite live region, never on first render.
  • While the browser prompt is up the Allow button stays focusable with aria-disabled, so focus never drops to the page when the state changes.
  • Steps are an ordered list. Menu paths read as "Settings then Privacy and security", with the chevrons hidden.
  • Blocked uses the red token, busy and missing the amber one, always with a crossed microphone glyph and the title as the backup.
  • The inline How to fix toggle sets aria-expanded and aria-controls.

Motion

  • The state glyph swaps with a 150 ms blur crossfade; recovery steps rise in over 180 ms.
  • While the prompt is up, the arrow nudges toward the address bar three times and stops. It never moves under reduced motion.
  • Nothing animates when the inline steps are toggled from the keyboard; they appear in place.