shadcn.io is an independent project built on shadcn/ui
A button that shows the selected microphone and opens a searchable list of every audio input the browser sees.
Use it before recording, where a person may have a laptop microphone, a headset and a webcam at once and the browser's default isn't the one they speak into. The list follows the hardware: plug a microphone in and it appears, unplug it and it goes. In Vue, it's a Popover holding a Command, and it hands you the chosen deviceId: recording from it, with getUserMedia, is yours.
Installation
Anatomy

MicSelector
├── MicSelectorTrigger
│ └── MicSelectorValue
└── MicSelectorContent
├── MicSelectorInput
└── MicSelectorList
├── MicSelectorEmpty
└── MicSelectorItem (one per microphone)
└── MicSelectorLabelMicSelectorList renders the items itself, one per microphone with its label; its children are what shows when the search matches none.
Usage
import { MicIcon } from "lucide-react";
import { useState } from "react";
import {
MicSelector, MicSelectorContent, MicSelectorEmpty, MicSelectorInput,
MicSelectorList, MicSelectorTrigger, MicSelectorValue,
} from "@/components/ui/mic-selector";export function MicrophonePicker() {
const [deviceId, setDeviceId] = useState<string>();
return (
<MicSelector onValueChange={setDeviceId} value={deviceId}>
<MicSelectorTrigger className="w-full">
<MicIcon aria-hidden="true" />
<MicSelectorValue />
</MicSelectorTrigger>
<MicSelectorContent>
<MicSelectorInput />
<MicSelectorList>
<MicSelectorEmpty />
</MicSelectorList>
</MicSelectorContent>
</MicSelector>
);
}Microphone access
Listing the microphones asks for nothing, but until the page may use the microphone the browser keeps their names to itself, and often the microphones too. So, until then, the list starts with an Allow microphone access row. Picking it shows the browser's prompt; once the person allows it, the list fills with every microphone by name. Opening the list never prompts on its own, and neither does loading the page.
Record from the chosen one by its deviceId:
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: { exact: deviceId } },
});A name that ends with a USB vendor and product id, such as Yeti Stereo Microphone (046d:0ab7), shows the id muted after the name.
Demos
Devices
devices lists microphones of your own instead of the browser's: here five, the Yeti selected, as a page shows them once access is allowed. Pass it to show a list you keep yourself, or in tests and previews where there's no microphone. Each one needs a deviceId and a label, which a browser's MediaDeviceInfo has.
Audio Devices
useAudioDevices reads the browser's microphones from inside a MicSelector, for a part of your own: here a line under the trigger with how many there are, and a button that asks for access. Its loadDevices is what the Allow microphone access row calls.
Labels
Every word it shows can change: placeholder on MicSelectorValue, placeholder on MicSelectorInput, permissionLabel on MicSelectorList, and the children of MicSelectorEmpty. Here they're in French.
useAudioDevices
Called inside a MicSelector, it returns the browser's microphones, whether or not devices replaces them in the list.
| Field | What it is |
|---|---|
devices | The audio inputs as MediaDeviceInfo, kept current on devicechange |
hasPermission | Whether the page may use the microphone, so the devices have names |
loadDevices | Asks for the microphone, then lists the devices again with their names |
loading | Whether they're being listed |
error | Why they couldn't be listed or access was refused, else null |
Migrating from AI SDK Elements
AI SDK Elements is for React. In Vue, Mic Selector has the same parts and props, and the demos above show them in Vue's syntax.
FAQ
Why does Mic Selector show Allow microphone access instead of my microphones?
Browsers hide microphones' names, and usually the microphones themselves, until the page may use one. Pick the row and allow access in the browser's prompt, and the list fills in. Once access is allowed, for this visit or for good, the row is gone.
How do I record from the microphone picked in Mic Selector?
onValueChange gives you its deviceId. Pass it to getUserMedia as audio: { deviceId: { exact: deviceId } }, as the Microphone access section shows. Keep the id: it usually stays the same for this site in that browser, so the next visit can select it again.
Does Mic Selector update when a microphone is plugged in?
Yes. It lists the devices again on the browser's devicechange event, so a headset plugged in or out appears or goes while the page is open. A selected microphone that's unplugged leaves the trigger showing the placeholder.
How do I show Mic Selector without real microphones?
Pass devices, an array of deviceId and label pairs, as the Devices demo does. The list shows those and skips the access row. It's how to keep a list of your own, or to render it in tests and screenshots where there's no hardware.
Can Mic Selector be used with the keyboard?
Yes. Enter or Space on the trigger opens the list with the search focused; typing filters by name, the arrow keys move, Enter picks, and Escape closes. Focus goes back to the trigger, which reads out the microphone it shows.
Is this the same as AI SDK Elements' Mic Selector?
It has the same parts and props, so most code only changes its import. The list renders its own items instead of taking a function, and it asks for the microphone from a row rather than on open. The Migrating section lists every difference.
API Reference
MicSelector
open?boolean
Whether it's open (controlled). Two-way with v-model:open.
defaultOpen?boolean
Whether it starts open, when uncontrolled.
- Default
false
triggerValue?string | null
The controlled trigger value. Two-way with v-model:triggerValue.
- Type
string | null
defaultTriggerValue?string | null
The initial trigger value when rendered. Use when you don't need to control the trigger value.
- Type
string | null
value?string
The selected microphone's deviceId (controlled). Two-way with v-model.
defaultValue?string
The microphone selected at first, when uncontrolled: its deviceId.
autoFocus?boolean
Whether to automatically set focus on the first focusable content within the popover when opened.
- Default
true
closeOnEscape?boolean
Whether to close the popover when the escape key is pressed.
- Default
true
closeOnInteractOutside?boolean
Whether to close the popover when the user clicks outside of the popover.
- Default
true
devices?MicSelectorDevice[]
The microphones to list instead of the browser's: your own list, or a demo's. Without it, it lists the browser's.
dir?"ltr" | "rtl"
The document's text/writing direction.
- Type
"ltr" | "rtl"- Default
"ltr"
finalFocusEl?() → MaybeElement
Element to receive focus when the popover is closed.
- Returns
MaybeElement
hideMode?"display-none" | "activity"
How to hide content when mounted but not present.
- display-none: HTML hidden attribute. Effects stay alive.
- activity: React 19 <Activity mode="hidden">. Effects pause. Requires React 19+.
- Type
"display-none" | "activity"- Default
"display-none"
id?string
The unique identifier of the machine.
ids?object
The ids of the elements in the popover. Useful for composition.
- Type
{ root?: string; label?: string; control?: string; input?: string; content?: string; trigger?: string; clearTrigger?: string; item?: (id: string, index?: number) → string; positioner?: string; itemGroup?: (id: string | number) → string; itemGroupLabel?: (id: string | number) → string }
immediate?boolean
Whether to synchronize the present change immediately or defer it to the next frame.
initialFocusEl?() → HTMLElement | null | false
The element to focus on when the popover is opened.
Return false to skip moving focus, or null to use the default.
- Type
() → HTMLElement | null | false- Returns
HTMLElement | null | false
lazyMount?boolean
Whether to enable lazy mounting.
- Default
false
modal?boolean
Whether the popover should be modal. When set to true:
- interaction with outside elements will be disabled
- only popover content will be visible to screen readers
- scrolling is blocked
- focus is trapped within the popover
- Default
false
onEnterComplete?VoidFunction
Function called when the animation ends in the open state.
onExitComplete?VoidFunction
Function called when the animation ends in the closed state.
persistentElements?Array<() → Element | null>
Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event
- Type
Array<() → Element | null>
portalled?boolean
Whether the popover is portalled. This will proxy the tabbing behavior regardless of the DOM position of the popover content.
- Default
true
positioning?object
The user provided options used to position the popover content.
- Type
{ restoreStyles?: boolean; applyStyles?: boolean; hideWhenDetached?: boolean; strategy?: "absolute" | "fixed"; placement?: Placement; offset?: { mainAxis?: number; crossAxis?: number; }; gutter?: number; shift?: number; overflowPadding?: number; arrowPadding?: number; flip?: boolean | Placement[]; slide?: boolean; overlap?: boolean; sameWidth?: boolean; fitViewport?: boolean; sizeMiddleware?: boolean; boundary?: (() → Boundary) | Boundary | "clipping-ancestors"; listeners?: boolean | AutoUpdateOptions; onComplete?: (data: ComputePositionReturn) → void; onPositioned?: ((data: { placed: boolean; }) → void); getAnchorElement?: () → HTMLElement | VirtualElement | null; getAnchorRect?: (element: HTMLElement | VirtualElement | null) → AnchorRect | null; updatePosition?: ((data: { updatePosition: () → Promise<void>; floatingElement: HTMLElement | null; }) → void | Promise<void>) }
present?boolean
Whether the node is present (controlled by the user).
restoreFocus?boolean
Whether to restore focus to the element that had focus before the popover was opened.
- Default
true
skipAnimationOnMount?boolean
Whether to allow the initial presence animation.
- Default
false
translations?object
Specifies the localized strings that identifies the accessibility elements and their states.
- Type
{ triggerLabel?: string; clearTriggerLabel?: string }
unmountOnExit?boolean
Whether to unmount on exit.
- Default
false
@open-change(open: boolean) → void
Called when it opens or closes.
- Parameters
open– boolean- Returns
void
@trigger-value-change(details: TriggerValueChangeDetails) → void
Function called when the trigger value changes.
- Type
(details: TriggerValueChangeDetails) → void- Parameters
details– { value: string | null; triggerElement: HTMLElement | null }- Returns
void
@value-change(value: string) → void
Called with the deviceId of the microphone picked.
- Parameters
value– string- Returns
void
@escape-key-down(event: KeyboardEvent) → void
Function called when the escape key is pressed.
- Parameters
event– { altKey: boolean; charCode: number; ctrlKey: boolean; code: string; getModifierState: (key: string) → boolean; key: string; keyCode: number; locale: string; location: number; metaKey: boolean; repeat: boolean; shiftKey: boolean; which: number }- Returns
void
@focus-outside(event: FocusOutsideEvent) → void
Function called when the focus is moved outside the component.
- Type
(event: FocusOutsideEvent) → void- Parameters
event– FocusOutsideEvent- Returns
void
@interact-outside(event: InteractOutsideEvent) → void
Function called when an interaction happens outside the component.
- Type
(event: InteractOutsideEvent) → void- Parameters
event– InteractOutsideEvent- Returns
void
@pointer-down-outside(event: PointerDownOutsideEvent) → void
Function called when the pointer is pressed down outside the component.
- Type
(event: PointerDownOutsideEvent) → void- Parameters
event– PointerDownOutsideEvent- Returns
void
@request-dismiss(event: LayerDismissEvent) → void
Function called when this layer is closed due to a parent layer being closed.
- Type
(event: LayerDismissEvent) → void- Parameters
event– LayerDismissEvent- Returns
void
MicSelectorTrigger
data-slot="mic-selector-trigger"
clickEffect?boolean
Apply a click effect to the button.
- Default
true
isLoading?boolean
Show a loading indicator.
- Default
false
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
MicSelectorContent
data-slot="mic-selector-content"
showCloseButton?boolean
Show close button at the top right corner.
- Default
true
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
--reference-widthstring
The trigger's width.
MicSelectorInput
data-slot="mic-selector-input"
size?InputProps["size"]
The size of the input.
- Default
"md"
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
MicSelectorList
data-slot="mic-selector-list"
permissionLabel?string
The row that asks for the microphone, shown until the page may use it.
- Default
"Allow microphone access"
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
class?string
Merged with the part's classes.
MicSelectorEmpty
data-slot="mic-selector-empty"
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
MicSelectorItem
data-slot="mic-selector-item"
valuestring
The microphone's deviceId.
persistFocus?boolean
Whether hovering outside should clear the highlighted state.
asChild?boolean
Use the provided child element as the default rendered element, combining their props and behavior.
MicSelectorLabel
<span> data-slot="mic-selector-label"
deviceobject
The microphone to name.
- Type
{ deviceId: string; label: string }
class?string
Merged with the part's classes.
MicSelectorValue
<span> data-slot="mic-selector-value"
placeholder?string
What it says before a microphone is selected.
- Default
"Select microphone..."
class?string
Merged with the part's classes.











