GitHub 0

Voice Picker

Previous Next

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

Prop Type Default Description
voices Voice[] — Voices to display in the picker. Any object conforming to Voice works.
value? string — Selected voice ID for controlled usage.
onValueChange? (value: string) => void — Called with the voice ID when the user picks a voice.
placeholder? string "Select a voice..." Placeholder shown on the trigger when no voice is selected.
class? string — Extra classes forwarded to the trigger button.
open? boolean — Controlled popover open state. Leave undefined for uncontrolled.
onOpenChange? (open: boolean) => void — Called when the popover open state changes.

Notes

  • Built on the shadcn-svelte Command and Popover primitives plus sv11's AudioPlayer — 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 previewUrl still render, but the hover play/pause overlay is suppressed.
  • Search filtering draws on each voice's name plus the common labels keys (accent, gender, age, description, use case).
  • Works controlled (value + onValueChange), uncontrolled (onValueChange only), or with an externally-driven popover (open + onOpenChange).
  • The Voice type is provider-agnostic — see Providers for the full shape and how to map your backend onto it.