Device Picker
The one picker for a screen, window, camera or microphone. A popover list with words for every state, so it's never blank while discovery runs or a device is gone.
Installation
$ pnpm dlx shadcn@latest add @videorc/device-pickerIt installs lib/source-select-state.ts, the picker's words, beside the component.
Usage
import { DevicePicker } from "@/components/device-picker";
import type { PickerDevice } from "@/components/device-picker";<DevicePicker
devices={cameras}
discoveryPending={!camerasListed}
kind="camera"
label="Camera"
noneLabel="Off"
onChange={setCameraId}
selectedName={savedCameraName}
value={cameraId}
/>A device is { id, name, kind, status, detail? }. status is "available", "permission-required" or "unavailable"; only an available device can be chosen, and the others say why in a muted word after their name. onChange gets the device id, or undefined for the "no device" item.
Use it everywhere a screen, window, camera or microphone is chosen, so they all say the same words. Don't use a Select for devices: it has no words for discovery, a missing device or a search.
Words for every state
The picker never shows a blank surface. With nothing chosen, discovery and an empty list win over the "no device" words, so a missing camera never hides behind "Off".
| State | The trigger says | The list says |
|---|---|---|
| Discovery hasn't reported | "Finding cameras…" | "Finding cameras…" |
| Nothing found | "No cameras found" | "No cameras found" |
| A device that can't run | Its name, then "Needs permission" or "Unavailable" | The same, disabled |
| A saved device that's gone | selectedName (or "Saved camera"), then "Not found" | The same, disabled |
| A search with no match | "No matching cameras" |
kind decides the nouns: capture for screens and windows, camera, or microphone. A saved device is only called "Not found" once discovery has reported.
Examples
Search, and screens and windows
searchable adds a search field that matches every word against a device's name, kind and detail (the app that owns a window), never its id. groupByKind puts screens and windows under their own headings when both are present.
Inline
trigger="inline" is a row's right-aligned value for a row whose label sits beside it. A long name truncates, and its title shows it whole.
The picker's words
lib/source-select-state.ts is pure, so every picker says the same thing in the same state, and the words are tested without a DOM.
| Export | What it does |
|---|---|
devicePickerTriggerView(input) | What the closed picker says: label, status and whether it's a placeholder |
devicePickerGroups(input) | The list: the "no device" item, a missing selection, then the matches |
devicePickerListStatus(input) | The one status line, or null |
deviceStatusLabel(status) | "Needs permission", "Unavailable", "Not found", or null |
devicePickerCopy(kind) | Every sentence for a kind |
sourceMatchesQuery(device, query) | The search rule |
DEVICE_PICKER_NONE_VALUE | The "no device" item's value |
Theming
| Token or attribute | Meaning |
|---|---|
selectTriggerClassName | The field trigger is exactly a Select's |
data-popup-open | On the trigger while the list is open; an inline trigger takes the hover block |
data-placeholder | A prompt or a state, in the secondary tone |
min-h-7 | List rows at the menu item height |
--anchor-width | The field list is as wide as its trigger |
Keyboard
| Key | Action |
|---|---|
| Enter Space | On the trigger, opens the list |
| ↑ ↓ | Moves the highlight, which starts on the current choice |
| Enter | Chooses the highlighted device |
| Esc | Closes without changing the choice, and clears the search |
Accessibility
The trigger is a combobox named by label, with aria-haspopup="listbox", or "dialog" with a search field. A field trigger is labelled by its visible label (labelHidden keeps it for screen readers only) and described by description, a polite live line for a switch in progress. Without a search field the list takes focus on open and names the highlighted row; with one, the field does. The list's status line is a status.
API reference
Prop
Type
Status Dot
Ambient status. The glass dot in its tone, pulsing while something runs, beside a quiet label. It replaces a loud header badge.
Power Slider
A labelled slider for one engine setting. The value with its unit or a typed number, a reset to the default, marks under the track, and a live preview apart from the commit.