A searchable voice selector with audio preview and Orb visualization. Provider-agnostic — pass any voices matching the Voice interface from ElevenLabs, OpenAI, Cartesia, or a custom backend.
Installation
npx shadcn-svelte@latest add https://sv11.ui.twango.dev/r/voice-picker.json Usage
<script lang="ts">
import { VoicePicker } from "$lib/registry/ui/voice-picker";
</script>
<VoicePicker /> Examples
Basic Usage
Pass an array of Voice objects and bind value/onValueChange to track the selected voice ID.
<script lang="ts">
import { VoicePicker, type Voice } from "$lib/registry/ui/voice-picker";
const voices: Voice[] = [
{
id: "21m00Tcm4TlvDq8ikWAM",
name: "Rachel",
previewUrl: "https://example.com/rachel-preview.mp3",
labels: { accent: "american", gender: "female", age: "young" },
},
];
let selectedVoice = $state("");
</script>
<VoicePicker {voices} value={selectedVoice} onValueChange={(v) => (selectedVoice = v)} /> Controlled vs Uncontrolled
Pass both value and onValueChange for a fully controlled picker, or just onValueChange to observe selections without owning the state.
<script lang="ts">
import { VoicePicker, type Voice } from "$lib/registry/ui/voice-picker";
let { voices }: { voices: Voice[] } = $props();
let selectedVoice = $state("");
</script>
<!-- Controlled -->
<VoicePicker {voices} value={selectedVoice} onValueChange={(v) => (selectedVoice = v)} />
<!-- Uncontrolled -->
<VoicePicker {voices} onValueChange={(voiceId) => console.log("Selected:", voiceId)} /> Control Open State
Pair open with onOpenChange to drive the popover externally — useful if you want to open the picker from another control.
<script lang="ts">
import { VoicePicker, type Voice } from "$lib/registry/ui/voice-picker";
let { voices }: { voices: Voice[] } = $props();
let open = $state(false);
let selectedVoice = $state("");
</script>
<VoicePicker
{voices}
{open}
onOpenChange={(o) => (open = o)}
value={selectedVoice}
onValueChange={(v) => (selectedVoice = v)}
/> Custom Placeholder
Override the trigger text shown before the user selects a voice.
<VoicePicker
{voices}
placeholder="Choose your voice..."
value={selectedVoice}
onValueChange={(v) => (selectedVoice = v)}
/> Loading Voices from a Provider
Map any provider's voice shape into the Voice interface once, then hand the array to VoicePicker. Below is a sketch for ElevenLabs — the same pattern applies to OpenAI, Cartesia, or a custom backend.
<script lang="ts">
import { onMount } from "svelte";
import { VoicePicker, type Voice } from "$lib/registry/ui/voice-picker";
let voices = $state<Voice[]>([]);
let selectedVoice = $state("");
onMount(async () => {
const res = await fetch("/api/voices");
const data = await res.json();
voices = data.voices.map((v: { voice_id: string; name: string; preview_url?: string }) => ({
id: v.voice_id,
name: v.name,
previewUrl: v.preview_url,
}));
});
</script>
<VoicePicker {voices} value={selectedVoice} onValueChange={(v) => (selectedVoice = v)} /> API Reference
Notes
- Built on the shadcn-svelte
CommandandPopoverprimitives plus sv11'sAudioPlayer— search, keyboard nav, and shared playback state come from those components. - Each row renders an
Orb; the selected voice also shows one on the trigger (hardcoded to the"thinking"state, purely decorative). - Preview playback is driven by
AudioPlayer, so only one voice plays at a time across the picker. - Voices without a
previewUrlstill render, but the hover play/pause overlay is suppressed. - Search filtering draws on each voice's
nameplus the commonlabelskeys (accent,gender,age,description,use case). - Works controlled (
value+onValueChange), uncontrolled (onValueChangeonly), or with an externally-driven popover (open+onOpenChange). - The
Voicetype is provider-agnostic — see Providers for the full shape and how to map your backend onto it.