Skip to content

Voice picker

Search, filter and preview voices, with accent and gender tags.

Status
Beta
Category
Voice
Adoption
Not used yet
import { VoicePicker } from "@oration/canon/components/voice-picker";
packages/canon/src/components/voice-picker.tsx

Voice

How Nora sounds on calls. Press play to hear a line in each voice.

6 voices

import { SettingsGroup, SettingsSection } from "@oration/canon/components/settings-section";import { type Voice, VoicePicker } from "@oration/canon/components/voice-picker";import * as React from "react";export function Hero() {    const voices: Voice[] = [        {            id: "nora",            name: "Nora",            gender: "female",            accent: "American",            age: "30s",            description:                "Warm and unhurried. Sounds like someone who has the answer in front of them.",            bestFor: ["Payment status", "Inbound support"],            provider: "ElevenLabs",            previewText:                "Thanks for calling Cedarline supplier support. I can check a payment for you right now.",        },        {            id: "callum",            name: "Callum",            gender: "male",            accent: "American",            age: "30s",            description:                "Clear and precise with even pacing. Reads long reference numbers well.",            bestFor: ["Remittance details", "Reading numbers"],            provider: "ElevenLabs",            previewText:                "The remittance for invoice 48213 went out by ACH on September 24 for 12,480 dollars.",        },        {            id: "imani",            name: "Imani",            gender: "female",            accent: "American",            age: "20s",            description: "Bright and encouraging without sounding scripted.",            bestFor: ["Vendor onboarding", "Web calls"],            provider: "ElevenLabs",        },        {            id: "rafael",            name: "Rafael",            gender: "male",            accent: "Mexican Spanish",            age: "40s",            description:                "Bilingual and reassuring. Switches between English and Spanish naturally.",            bestFor: ["Spanish line", "Language mirroring"],            provider: "ElevenLabs",            previewText:                "Gracias por llamar a Cedarline. ¿Me puede dar el número de factura, por favor?",        },        {            id: "declan",            name: "Declan",            gender: "male",            accent: "Irish",            age: "40s",            description:                "Easygoing and patient. Good for long reconciliation calls.",            bestFor: ["Statement reconciliation"],            provider: "ElevenLabs",        },        {            id: "grace",            name: "Grace",            gender: "female",            accent: "British",            age: "50s",            description:                "Calm and formal. Suits escalations and overdue balances.",            bestFor: ["Escalations", "Collections"],            provider: "ElevenLabs",        },    ];    const [value, setValue] = React.useState("nora");    return (        <SettingsSection            title="Voice"            description="How Nora sounds on calls. Press play to hear a line in each voice."            className="w-full max-w-2xl py-0"        >            <SettingsGroup className="p-4">                <VoicePicker                    voices={voices}                    value={value}                    onChange={setValue}                />            </SettingsGroup>        </SettingsSection>    );}

Usage#

Voice picker is how people choose what an agent sounds like: a searchable list of voices with gender and accent filters, a short preview on every row and use-case tags. In Oration it sits in an agent's voice settings and in onboarding. The mistake is choosing from names alone; each row should say how the voice sounds and which calls it suits, and people should hear it say a real line before they pick.

When to use

  • To choose an agent's text-to-speech voice from the selected provider's catalog.
  • When people need to hear a voice before choosing, saying a line the agent would actually say.
  • When the catalog is long enough to search and filter, roughly more than five voices.
  • In onboarding, to give the first agent a voice.

When not to use

  • For choosing the speech provider and model the voices come from. Use Provider model picker
  • For a short list of plain options with no previews. Use Radio group
  • For two to four rich options that aren't voices. Use Choice card
  • For showing an agent speaking during a live call. Use Voice orb
  • For playing back a recorded call. Use Waveform

Hear it before you pick it

Every row has a preview. Set previewText to a line from the agent's real calls, with the numbers and names it must read well: The remittance for invoice 48213 went out by ACH on September 24.

The Label-Beside-Color Rule

Selection is a tint, an inset edge and aria-checked together. Playing swaps the icon to Stop, shows the level meter and announces Playing.

The Ink Fill Rule

The playing meter is five bars in ink at 65%, never indigo. It shows activity, not a choice.

Anatomy#

  1. Search. An input group with a search icon and Search by name, accent or use.
  2. Filter chips. 28px pressed toggles for gender, then accent. A group only appears when it has two or more values.
  3. Count. 12px tabular text: 6 voices, or 2 of 6 voices while filtering.
  4. Voice row. 8px padding and rounded-lg. The selected row gets a 6% indigo tint and a 22% indigo inset edge.
  5. Preview button. A 32px ghost icon button: Play, or a filled Stop while playing.
  6. Name. 14px at weight 500.
  7. Meta. Gender, accent and age in 12px muted text.
  8. Playing meter. Five 2px ink bars beside the meta while the preview plays.
  9. Provider. 12px muted text at the end of the name row.
  10. Description. 13px muted text: how the voice sounds and where it fits.
  11. Best-for tags. 20px gray tags for the calls it suits.

Examples#

Spoken previews

With speakPreview, the browser reads each voice's previewText aloud where speech synthesis is available. Write a line with the numbers and names the agent must say well.

2 voices

import { type Voice, VoicePicker } from "@oration/canon/components/voice-picker";import * as React from "react";export function SpokenPreview() {    const voices: Voice[] = [        {            id: "callum",            name: "Callum",            gender: "male",            accent: "American",            age: "30s",            description:                "Clear and precise with even pacing. Reads long reference numbers well.",            bestFor: ["Remittance details"],            previewText:                "The remittance for invoice 48213 went out by ACH on September 24 for 12,480 dollars.",        },        {            id: "nora",            name: "Nora",            gender: "female",            accent: "American",            age: "30s",            description: "Warm and unhurried.",            bestFor: ["Payment status"],            previewText:                "Invoice INV-20931 from Northwind Freight is scheduled for Friday, October 2.",        },    ];    const [value, setValue] = React.useState("callum");    return (        <VoicePicker            voices={voices}            value={value}            onChange={setValue}            speakPreview            className="w-full max-w-xl"        />    );}

Filters only when they help

A filter group appears only when it has two or more values. Every voice here is American, so only the gender chips show.

3 voices

import { type Voice, VoicePicker } from "@oration/canon/components/voice-picker";import * as React from "react";export function FewVoices() {    const voices: Voice[] = [        {            id: "nora",            name: "Nora",            gender: "female",            accent: "American",            description: "Warm and unhurried.",            bestFor: ["Payment status"],        },        {            id: "callum",            name: "Callum",            gender: "male",            accent: "American",            description: "Clear and precise with even pacing.",            bestFor: ["Remittance details"],        },        {            id: "imani",            name: "Imani",            gender: "female",            accent: "American",            description: "Bright and encouraging without sounding scripted.",            bestFor: ["Vendor onboarding"],        },    ];    const [value, setValue] = React.useState("imani");    return (        <VoicePicker            voices={voices}            value={value}            onChange={setValue}            className="w-full max-w-xl"        />    );}

In onboarding

A short catalog inside a step, renamed with label and followed by the step's one filled button.

Choose a voice for your supplier line

You can change it later in the agent's voice settings.

3 voices

import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import { type Voice, VoicePicker } from "@oration/canon/components/voice-picker";import * as React from "react";export function Onboarding() {    const voices: Voice[] = [        {            id: "nora",            name: "Nora",            gender: "female",            accent: "American",            age: "30s",            description:                "Warm and unhurried. Sounds like someone who has the answer in front of them.",            bestFor: ["Payment status"],        },        {            id: "callum",            name: "Callum",            gender: "male",            accent: "American",            age: "30s",            description:                "Clear and precise. Reads long reference numbers well.",            bestFor: ["Remittance details"],        },        {            id: "grace",            name: "Grace",            gender: "female",            accent: "British",            age: "50s",            description:                "Calm and formal. Suits escalations and overdue balances.",            bestFor: ["Escalations"],        },    ];    const [value, setValue] = React.useState("nora");    const chosen = voices.find((voice) => voice.id === value);    return (        <div className="flex w-full max-w-xl flex-col overflow-hidden rounded-xl bg-card shadow-border">            <div className="flex flex-col gap-1 px-5 pt-5 pb-3">                <p className="text-base font-medium text-foreground">                    Choose a voice for your supplier line                </p>                <p className="text-13 text-muted-foreground">                    You can change it later in the agent's voice settings.                </p>            </div>            <div className="px-5 pb-4">                <VoicePicker                    voices={voices}                    value={value}                    onChange={setValue}                    label="Agent voice"                />            </div>            <div className="flex items-center justify-end gap-2 border-t border-border bg-muted/50 px-5 py-3">                <Button                    type="button"                    variant="ghost"                    onClick={() =>                        toast.add({ title: "Back to naming your agent" })                    }                >                    Back                </Button>                <Button                    type="button"                    onClick={() =>                        toast.add({                            type: "success",                            title: `${chosen?.name ?? "Voice"} it is`,                            description:                                "Next, connect the number suppliers call.",                        })                    }                >                    Continue                </Button>            </div>        </div>    );}

Disabled

The voice radios are inert; previews still play. Say who can change the voice.

2 voices

Only admins can change Nora's voice. Ask Maya Okafor for access.

import { type Voice, VoicePicker } from "@oration/canon/components/voice-picker";import * as React from "react";export function Disabled() {    const voices: Voice[] = [        {            id: "nora",            name: "Nora",            gender: "female",            accent: "American",            age: "30s",            description: "Warm and unhurried.",            bestFor: ["Payment status"],            provider: "ElevenLabs",        },        {            id: "callum",            name: "Callum",            gender: "male",            accent: "American",            age: "30s",            description: "Clear and precise with even pacing.",            bestFor: ["Remittance details"],            provider: "ElevenLabs",        },    ];    const [value, setValue] = React.useState("nora");    return (        <div className="flex w-full max-w-xl flex-col gap-2">            <VoicePicker                voices={voices}                value={value}                onChange={setValue}                disabled            />            <p className="text-13 text-muted-foreground">                Only admins can change Nora's voice. Ask Maya Okafor for access.            </p>        </div>    );}

States#

States
StateTreatment
RestRows on the surface with no fill.
HoverA shared highlight follows the pointer down the list.
Selected6% indigo tint, 22% indigo inset edge and aria-checked.
Focus visibleA 3px Focus Indigo ring at 40% on the row's text, or on the preview button.
PlayingThe button shows a filled square in ink and is named Stop, the meter animates beside the meta and Playing is announced.
Filter chip pressedThe same indigo tint and edge as a selected row. At rest chips are Well Gray at 70%; they scale to 0.96 while pressed.
FilteredThe count reads 2 of 6 voices.
No matchesA well with No voices match "Scottish". (or these filters) and Clear filters.
DisabledVoice rows at 50% and inert; the list carries aria-disabled.

Behavior#

  • It is controlled: value is the selected voice id and onChange gets the new id.
  • Search matches name, accent, gender, age, description, provider and best-for tags, ignoring case.
  • Chips within a group widen the match (American or British); groups narrow it (female and British). Clear filters resets search and both groups.
  • One preview plays at a time. Playing another stops the first, and pressing the playing one stops it. Unmounting stops playback.
  • Without speakPreview, a preview is simulated for three seconds. With it, and where the browser supports speech synthesis, the browser speaks previewText, or Hi, I'm Nora. How can I help you today?, and stops when it finishes.
  • Filtering never changes the selection. If the selected voice is hidden, the first visible row takes the tab stop.
  • The list is a roving radio group: arrow keys move and select, wrapping at the ends, and Home and End jump. Clicking a row's padding selects it too.

Do and don't#

1 voice

Do. Describe how the voice sounds and where it shines, in a short sentence or two.

1 voice

Don't. Repeat the meta as the description. Female American voice tells people nothing the row doesn't already say.

1 voice

Do. Tag one to three call types the voice suits, in Cedarline's words.

1 voice

Don't. Pile on generic tags. Six adjectives make every voice look the same.

Content#

  • Voice names are first names: Nora, Callum, Rafael.
  • Descriptions are how it sounds, then where it fits: Clear and precise with even pacing. Reads long reference numbers well.
  • Best-for tags are call types: Payment status, Remittance details, Spanish line.
  • Accents are written as people say them: American, British, Mexican Spanish. Ages are decades: 30s.
  • Preview lines come from real calls and include an amount, a date or an invoice number.

Accessibility#

  • The list is a role="radiogroup" named by label (default Voice). Rows are radios; their name is the voice name and the rest of the row.
  • Search is named Search voices and controls the list. The count is a polite live region, so filtering is announced.
  • Filter chips are toggle buttons with aria-pressed, grouped as Gender and Accent.
  • Preview buttons are named Play Nora preview and Stop Nora preview. The meter is aria-hidden; a visually hidden Playing carries the state.
  • Best-for tags are introduced with a visually hidden Best for:.
  • Only the selected row's preview button is in the tab order, so previewing another voice by keyboard means arrowing to it, which selects it.
Keyboard interactions
KeysAction
TabMoves through search, filter chips, then the selected row's preview button and radio.
↑↓Move to and select the previous or next voice. Left and Right do the same.
HomeSelects the first visible voice. End selects the last.
SpaceToggles a focused chip, or plays and stops a focused preview. Enter does the same.

Design tokens#

Design tokens
TokenUsed for
--primarySelected row and pressed chip: 6% tint and 22% inset edge
--mutedFilter chips at 70%, solid on hover; the no-matches well
--foregroundPlaying meter bars at 65%
--tag-grayBest-for tags
--muted-foregroundMeta, provider, description, count, idle preview icon
--ringFocus ring at 40%

API reference#

VoicePicker

Search, filters and a radio list of voices with previews.

Other props spread onto Doesn't spread props; renders <div data-slot="voice-picker">.

Props of VoicePicker
PropTypeDefaultDescription
voicesRequiredVoice[]No defaultThe catalog, in display order.
valueRequiredstringNo defaultId of the selected voice.
onChangeRequired(id: string) => voidNo defaultCalled with the newly selected id.
speakPreviewbooleanfalseSpeak previews with the browser's speech synthesis instead of simulating them.
labelstring"Voice"Accessible name of the list.
disabledbooleanNo defaultDisables the voice radios.
classNamestringNo defaultMerged onto the root.

Voice

One voice.

Props of Voice
PropTypeDefaultDescription
idRequiredstringNo defaultStable id, used as the value.
nameRequiredstringNo defaultThe voice's name.
descriptionRequiredstringNo defaultHow it sounds and where it fits.
genderstringNo defaultFilterable. Compared lowercased and shown capitalized.
accentstringNo defaultFilterable, shown as written.
agestringNo defaultA decade, like 30s.
bestForstring[]No defaultCall types it suits, shown as tags.
providerstringNo defaultShown at the end of the name row.
previewTextstringNo defaultWhat the preview says when spoken.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

Meta is joined with middle dots (Female · American · 30s), which Canon's writing rules ban.

disabled only disables the voice radios. Search, filter chips and previews keep working.

Previews share the roving tab stop with the radios, so only the selected row's preview is reachable by Tab.

The level meter's comment says it holds still under reduced motion, but it animates regardless; it has no reduced-motion check of its own.

Filter chips pass text-13 next to a text color through the cn package, which drops the size, and they are a local copy rather than the Filter chip component.

The app passes the provider id as provider, so rows in agent settings read elevenlabs rather than ElevenLabs.

Simulated previews make no sound; only speakPreview produces audio, and nothing plays the provider's real sample.