The VoiceSelector component provides a flexible and composable interface for selecting AI voices. Built on shadcn-vue's Dialog and Command components, it features a searchable voice list with support for metadata display (gender, accent, age), grouping, and customizable layouts. The component includes a context provider for accessing voice selection state from any nested component.
AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add voice-selector
npx shadcn-vue@latest add https://registry.ai-elements-vue.com/voice-selector.json
Copy and paste the following code into your project.
VoiceSelector.vue
VoiceSelectorTrigger.vue
VoiceSelectorContent.vue
VoiceSelectorDialog.vue
VoiceSelectorInput.vue
VoiceSelectorList.vue
VoiceSelectorEmpty.vue
VoiceSelectorGroup.vue
VoiceSelectorItem.vue
VoiceSelectorShortcut.vue
VoiceSelectorSeparator.vue
VoiceSelectorGender.vue
VoiceSelectorAccent.vue
VoiceSelectorAge.vue
VoiceSelectorName.vue
VoiceSelectorDescription.vue
VoiceSelectorAttributes.vue
VoiceSelectorBullet.vue
VoiceSelectorPreview.vue
context.ts
index.ts
< script setup lang = "ts" >
import { Dialog } from '@repo/shadcn-vue/components/ui/dialog'
import { useVModel } from '@vueuse/core'
import { provide } from 'vue'
import { VoiceSelectorKey } from './context'
type VoiceSelectorProps = InstanceType < typeof Dialog>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorProps {
value ?: string
defaultValue ?: string
open ?: boolean
defaultOpen ?: boolean
}
const props = withDefaults ( defineProps < Props >(), {
open: undefined ,
defaultOpen: false ,
})
const emit = defineEmits <{
( e : 'update:value' , value : string | undefined ) : void
( e : 'update:open' , open : boolean ) : void
( e : 'valueChange' , value : string | undefined ) : void
( e : 'openChange' , open : boolean ) : void
}>()
const value = useVModel (props, 'value' , emit, {
defaultValue: props.defaultValue,
passive: (props.value === undefined ) as any ,
})
const open = useVModel (props, 'open' , emit, {
defaultValue: props.defaultOpen,
passive: (props.open === undefined ) as any ,
})
function setValue ( newValue : string | undefined ) {
value.value = newValue
emit ( 'valueChange' , newValue)
}
function setOpen ( newOpen : boolean ) {
open.value = newOpen
emit ( 'openChange' , newOpen)
}
provide (VoiceSelectorKey, {
value,
setValue,
open,
setOpen,
})
</ script >
< template >
< Dialog
:open = "open"
@update:open = "setOpen"
>
< slot / >
</ Dialog >
</ template >
Expand
< script setup lang = "ts" >
import { DialogTrigger } from '@repo/shadcn-vue/components/ui/dialog'
type VoiceSelectorTriggerProps = InstanceType < typeof DialogTrigger>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorTriggerProps {}
defineProps < Props >()
</ script >
< template >
< DialogTrigger >
< slot / >
</ DialogTrigger >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { Command } from '@repo/shadcn-vue/components/ui/command'
import {
DialogContent,
DialogTitle,
} from '@repo/shadcn-vue/components/ui/dialog'
import { cn } from '@repo/shadcn-vue/lib/utils'
type VoiceSelectorContentProps = InstanceType < typeof DialogContent>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorContentProps {
class ?: HTMLAttributes [ 'class' ]
title ?: string
}
const props = withDefaults ( defineProps < Props >(), {
title: 'Voice Selector' ,
})
</ script >
< template >
< DialogContent
:class = "cn('p-0', props.class)"
>
< DialogTitle class = "sr-only" >
{{ title }}
</ DialogTitle >
< Command class = "**:data-[slot=command-input-wrapper]:h-auto" >
< slot / >
</ Command >
</ DialogContent >
</ template >
Expand
< script setup lang = "ts" >
import { CommandDialog } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorDialogProps = InstanceType < typeof CommandDialog>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorDialogProps {}
defineProps < Props >()
</ script >
< template >
< CommandDialog >
< slot / >
</ CommandDialog >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { CommandInput } from '@repo/shadcn-vue/components/ui/command'
import { cn } from '@repo/shadcn-vue/lib/utils'
type VoiceSelectorInputProps = InstanceType < typeof CommandInput>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorInputProps {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< CommandInput
:class = "cn('h-auto py-3.5', props.class)"
/>
</ template >
Expand
< script setup lang = "ts" >
import { CommandList } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorListProps = InstanceType < typeof CommandList>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorListProps {}
defineProps < Props >()
</ script >
< template >
< CommandList >
< slot / >
</ CommandList >
</ template >
Expand
< script setup lang = "ts" >
import { CommandEmpty } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorEmptyProps = InstanceType < typeof CommandEmpty>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorEmptyProps {}
defineProps < Props >()
</ script >
< template >
< CommandEmpty >
< slot / >
</ CommandEmpty >
</ template >
Expand
< script setup lang = "ts" >
import { CommandGroup } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorGroupProps = InstanceType < typeof CommandGroup>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorGroupProps {}
defineProps < Props >()
</ script >
< template >
< CommandGroup >
< slot / >
</ CommandGroup >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { CommandItem } from '@repo/shadcn-vue/components/ui/command'
import { cn } from '@repo/shadcn-vue/lib/utils'
type VoiceSelectorItemProps = InstanceType < typeof CommandItem>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorItemProps {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
const { class : _ , ... rest } = props
</ script >
< template >
< CommandItem
:class = "cn('hover:bg-accent hover:text-accent-foreground px-4 py-2', props.class)"
v-bind = "rest"
>
< slot / >
</ CommandItem >
</ template >
Expand
< script setup lang = "ts" >
import { CommandShortcut } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorShortcutProps = InstanceType < typeof CommandShortcut>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorShortcutProps {}
defineProps < Props >()
</ script >
< template >
< CommandShortcut >
< slot / >
</ CommandShortcut >
</ template >
Expand
< script setup lang = "ts" >
import { CommandSeparator } from '@repo/shadcn-vue/components/ui/command'
type VoiceSelectorSeparatorProps = InstanceType < typeof CommandSeparator>[ '$props' ]
interface Props extends /* @vue-ignore */ VoiceSelectorSeparatorProps {}
defineProps < Props >()
</ script >
< template >
< CommandSeparator >
< slot / >
</ CommandSeparator >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import {
CircleSmallIcon,
MarsIcon,
MarsStrokeIcon,
NonBinaryIcon,
TransgenderIcon,
VenusAndMarsIcon,
VenusIcon,
} from 'lucide-vue-next'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
value ?:
| 'male'
| 'female'
| 'transgender'
| 'androgyne'
| 'non-binary'
| 'intersex'
}
const props = defineProps < Props >()
</ script >
< template >
< span
:class = "cn('text-muted-foreground text-xs', props.class)"
>
< slot >
< MarsIcon v-if = "props.value === 'male'" class = "size-4" />
< VenusIcon v-else-if = "props.value === 'female'" class = "size-4" />
< TransgenderIcon v-else-if = "props.value === 'transgender'" class = "size-4" />
< MarsStrokeIcon v-else-if = "props.value === 'androgyne'" class = "size-4" />
< NonBinaryIcon v-else-if = "props.value === 'non-binary'" class = "size-4" />
< VenusAndMarsIcon v-else-if = "props.value === 'intersex'" class = "size-4" />
< CircleSmallIcon v-else class = "size-1" fill = "currentColor" />
</ slot >
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { computed } from 'vue'
type Accent
= | 'american'
| 'british'
| 'australian'
| 'canadian'
| 'irish'
| 'scottish'
| 'indian'
| 'south-african'
| 'new-zealand'
| 'spanish'
| 'french'
| 'german'
| 'italian'
| 'portuguese'
| 'brazilian'
| 'mexican'
| 'argentinian'
| 'japanese'
| 'chinese'
| 'korean'
| 'russian'
| 'arabic'
| 'dutch'
| 'swedish'
| 'norwegian'
| 'danish'
| 'finnish'
| 'polish'
| 'turkish'
| 'greek'
| ( string & {})
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
value ?: Accent
}
const props = defineProps < Props >()
const emoji = computed (() => {
switch (props.value) {
case 'american' :
return 'đşđ¸'
case 'british' :
return 'đŹđ§'
case 'australian' :
return 'đŚđş'
case 'canadian' :
return 'đ¨đŚ'
case 'irish' :
return 'đŽđŞ'
case 'scottish' :
return 'đ´ó §ó ˘ó łó Łó ´ó ż'
case 'indian' :
return 'đŽđł'
case 'south-african' :
return 'đżđŚ'
case 'new-zealand' :
return 'đłđż'
case 'spanish' :
return 'đŞđ¸'
case 'french' :
return 'đŤđˇ'
case 'german' :
return 'đŠđŞ'
case 'italian' :
return 'đŽđš'
case 'portuguese' :
return 'đľđš'
case 'brazilian' :
return 'đ§đˇ'
case 'mexican' :
return 'đ˛đ˝'
case 'argentinian' :
return 'đŚđˇ'
case 'japanese' :
return 'đŻđľ'
case 'chinese' :
return 'đ¨đł'
case 'korean' :
return 'đ°đˇ'
case 'russian' :
return 'đˇđş'
case 'arabic' :
return 'đ¸đŚ'
case 'dutch' :
return 'đłđą'
case 'swedish' :
return 'đ¸đŞ'
case 'norwegian' :
return 'đłđ´'
case 'danish' :
return 'đŠđ°'
case 'finnish' :
return 'đŤđŽ'
case 'polish' :
return 'đľđą'
case 'turkish' :
return 'đšđˇ'
case 'greek' :
return 'đŹđˇ'
default :
return null
}
})
</ script >
< template >
< span
:class = "cn('text-muted-foreground text-xs', props.class)"
>
< slot >
{{ emoji }}
</ slot >
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< span
:class = "cn('text-muted-foreground text-xs tabular-nums', props.class)"
>
< slot / >
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< span
:class = "cn('flex-1 truncate text-left font-medium', props.class)"
>
< slot / >
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< span
:class = "cn('text-muted-foreground text-xs', props.class)"
>
< slot / >
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< div
:class = "cn('flex items-center text-xs', props.class)"
>
< slot / >
</ div >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
interface Props extends /* @vue-ignore */ HTMLAttributes {
class ?: HTMLAttributes [ 'class' ]
}
const props = defineProps < Props >()
</ script >
< template >
< span
aria-hidden = "true"
:class = "cn('select-none text-border', props.class)"
>
•
</ span >
</ template >
Expand
< script setup lang = "ts" >
import type { HTMLAttributes } from 'vue'
import { Button } from '@repo/shadcn-vue/components/ui/button'
import { Spinner } from '@repo/shadcn-vue/components/ui/spinner'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { PauseIcon, PlayIcon } from 'lucide-vue-next'
interface Props {
class ?: HTMLAttributes [ 'class' ]
playing ?: boolean
loading ?: boolean
}
const props = defineProps < Props >()
const emit = defineEmits <{
( e : 'play' ) : void
}>()
function handleClick ( event : MouseEvent ) {
event. stopPropagation ()
emit ( 'play' )
}
</ script >
< template >
< Button
:aria-label = "playing ? 'Pause preview' : 'Play preview'"
:class = "cn('size-6', props.class)"
:disabled = "loading"
size = "icon-sm"
type = "button"
variant = "outline"
@click = "handleClick"
>
< Spinner v-if = "loading" class = "size-3" />
< PauseIcon v-else-if = "playing" class = "size-3" />
< PlayIcon v-else class = "size-3" />
</ Button >
</ template >
Expand
import type { InjectionKey, Ref } from 'vue'
import { inject } from 'vue'
export interface VoiceSelectorContextValue {
value : Ref < string | undefined >
setValue : ( value : string | undefined ) => void
open : Ref < boolean | undefined >
setOpen : ( open : boolean ) => void
}
export const VoiceSelectorKey : InjectionKey < VoiceSelectorContextValue > = Symbol ( 'VoiceSelector' )
export function useVoiceSelector ( componentName : string ) : VoiceSelectorContextValue {
const context = inject (VoiceSelectorKey)
if ( ! context) {
throw new Error ( `${ componentName } must be used within VoiceSelector` )
}
return context
}
export * from './context'
export { default as VoiceSelector } from './VoiceSelector.vue'
export { default as VoiceSelectorAccent } from './VoiceSelectorAccent.vue'
export { default as VoiceSelectorAge } from './VoiceSelectorAge.vue'
export { default as VoiceSelectorAttributes } from './VoiceSelectorAttributes.vue'
export { default as VoiceSelectorBullet } from './VoiceSelectorBullet.vue'
export { default as VoiceSelectorContent } from './VoiceSelectorContent.vue'
export { default as VoiceSelectorDescription } from './VoiceSelectorDescription.vue'
export { default as VoiceSelectorDialog } from './VoiceSelectorDialog.vue'
export { default as VoiceSelectorEmpty } from './VoiceSelectorEmpty.vue'
export { default as VoiceSelectorGender } from './VoiceSelectorGender.vue'
export { default as VoiceSelectorGroup } from './VoiceSelectorGroup.vue'
export { default as VoiceSelectorInput } from './VoiceSelectorInput.vue'
export { default as VoiceSelectorItem } from './VoiceSelectorItem.vue'
export { default as VoiceSelectorList } from './VoiceSelectorList.vue'
export { default as VoiceSelectorName } from './VoiceSelectorName.vue'
export { default as VoiceSelectorPreview } from './VoiceSelectorPreview.vue'
export { default as VoiceSelectorSeparator } from './VoiceSelectorSeparator.vue'
export { default as VoiceSelectorShortcut } from './VoiceSelectorShortcut.vue'
export { default as VoiceSelectorTrigger } from './VoiceSelectorTrigger.vue'
Fully composable architecture with granular control components Built on shadcn-vue Dialog and Command components Vue Provide/Inject API for accessing state in nested components Searchable voice list with real-time filtering Support for voice metadata with icons and emojis (gender icons, accent flags, age) Voice preview button with play/pause/loading states Voice grouping with separators and bullet dividers Keyboard navigation support Controlled and uncontrolled component patterns Full TypeScript support with proper types for all components Root Dialog component that provides context for all child components. Manages both voice selection and dialog open states.
The selected voice ID (controlled). The default selected voice ID (uncontrolled). The open state of the dialog (controlled). The default open state (uncontrolled). Any other props are spread to the Dialog component. Button or element that opens the voice selector dialog.
Change the default rendered element for the one passed as a child, merging their props and behavior. ...props DialogTriggerProps
Any other props are spread to the DialogTrigger component. Container for the Command component and voice list, rendered inside the dialog.
The title for screen readers. Hidden visually but accessible to assistive technologies. Additional CSS classes to apply to the dialog content. ...props DialogContentProps
Any other props are spread to the DialogContent component. Alternative dialog implementation using CommandDialog for a full-screen command palette style.
...props CommandDialogProps
Any other props are spread to the CommandDialog component. Search input for filtering voices.
Placeholder text for the search input. Additional CSS classes to apply. ...props CommandInputProps
Any other props are spread to the CommandInput component. Scrollable container for voice items and groups.
Any other props are spread to the CommandList component. Message shown when no voices match the search query.
...props CommandEmptyProps
Any other props are spread to the CommandEmpty component. Groups related voices together with an optional heading.
The heading text for the group. ...props CommandGroupProps
Any other props are spread to the CommandGroup component. Selectable item representing a voice.
The unique identifier for this voice. Used for search filtering. Additional CSS classes to apply. Any other props are spread to the CommandItem component. Visual separator between voice groups.
...props CommandSeparatorProps
Any other props are spread to the CommandSeparator component. Displays keyboard shortcuts for voice items.
...props CommandShortcutProps
Any other props are spread to the CommandShortcut component. Displays the voice name with proper styling.
Additional CSS classes to apply. Displays the voice gender metadata with icons from Lucide. Supports multiple gender identities with corresponding icons.
value "male" | "female" | "transgender" | "androgyne" | "non-binary" | "intersex"
The gender value that determines which icon to display. Supported values: "male" (Mars), "female" (Venus), "transgender", "androgyne", "non-binary", "intersex". Defaults to a small circle if no value matches. Additional CSS classes to apply. Displays the voice accent metadata with emoji flags representing different countries/regions.
The accent value that determines which flag emoji to display. Supports 27 different accents including: "american" đşđ¸, "british" đŹđ§, "australian" đŚđş, "canadian" đ¨đŚ, "irish" đŽđŞ, "scottish" đ´ó §ó ˘ó łó Łó ´ó ż, "indian" đŽđł, "south-african" đżđŚ, "new-zealand" đłđż, "spanish" đŞđ¸, "french" đŤđˇ, "german" đŠđŞ, "italian" đŽđš, "portuguese" đľđš, "brazilian" đ§đˇ, "mexican" đ˛đ˝, "argentinian" đŚđˇ, "japanese" đŻđľ, "chinese" đ¨đł, "korean" đ°đˇ, "russian" đˇđş, "arabic" đ¸đŚ, "dutch" đłđą, "swedish" đ¸đŞ, "norwegian" đłđ´, "danish" đŠđ°, "finnish" đŤđŽ, "polish" đľđą, "turkish" đšđˇ, "greek" đŹđˇ. Also accepts any custom string value. Additional CSS classes to apply. Displays the voice age metadata with muted styling and tabular numbers for consistent alignment.
Additional CSS classes to apply. Displays a description for the voice with muted styling.
Additional CSS classes to apply. Container for grouping voice attributes (gender, accent, age) together. Use with VoiceSelectorBullet for separation.
Additional CSS classes to apply. Displays a bullet separator (â˘) between voice attributes. Hidden from screen readers via aria-hidden.
Additional CSS classes to apply. A button that allows users to preview/play a voice sample before selecting it. Shows play, pause, or loading icons based on state.
Whether the voice is currently playing. Shows pause icon when true. Whether the voice preview is loading. Shows loading spinner and disables the button. Additional CSS classes to apply. update:value string | undefined
Emitted when the selected voice changes (for v-model). valueChange string | undefined
Callback emitted when the selected voice changes. Emitted when the open state changes (for v-model). Callback emitted when the open state changes. Emitted when the voice is selected with the voice value. Emitted when the preview button is clicked. A custom composable for accessing the voice selector context. This composable allows you to access and control the voice selection state from any component nested within VoiceSelector.
< script setup lang = "ts" >
import { useVoiceSelector } from '@repo/elements/voice-selector'
const { value , setValue , open , setOpen } = useVoiceSelector ( 'MyComponent' )
</ script >
< template >
< div >
< p >Selected voice: {{ value ?? 'None' }}</ p >
< button @click = "setOpen(!open)" >
Toggle Dialog
</ button >
</ div >
</ template >
value Ref<string | undefined>
The currently selected voice ID. setValue (value: string | undefined) => void
Function to update the selected voice ID. open Ref<boolean | undefined>
Whether the dialog is currently open. setOpen (open: boolean) => void
Function to control the dialog open state.