GitHub 0

Speech Input

Previous Next

A compact speech-to-text input component with a provider-agnostic transcription adapter. Ships with a Web Speech API adapter for demos; users supply their own for ElevenLabs Scribe, Deepgram, or other providers.

SpeechInput is provider-agnostic — pass a TranscriptionAdapter wired to ElevenLabs, Deepgram, OpenAI, or any other backend. This demo has no STT hooked up.

Installation

npx shadcn-svelte@latest add https://sv11.ui.twango.dev/r/speech-input.json

Usage

<script lang="ts">
	import { SpeechInput } from "$lib/registry/ui/speech-input";
</script>
 
<SpeechInput />

<SpeechInput> requires an adapter prop — see Providers for the interface and Adapters for provider recipes.

Examples

Basic Usage

Compose SpeechInput with the record button, preview, and cancel button. Pass any object that matches TranscriptionAdapter.

<script lang="ts">
	import * as SpeechInput from "$lib/registry/ui/speech-input";
	import type { TranscriptionAdapter } from "$lib/registry/ui/speech-input";
 
	const adapter: TranscriptionAdapter = createMyAdapter(/* ... */);
</script>
 
<SpeechInput.Root
	{adapter}
	onChange={(data) => console.log(data.transcript)}
	onStop={(data) => console.log("Final:", data.transcript)}
>
	<SpeechInput.RecordButton />
	<SpeechInput.Preview placeholder="Start speaking..." />
	<SpeechInput.CancelButton />
</SpeechInput.Root>

With Form Input

Use onStop to append the committed transcript onto an external text field.

<script lang="ts">
	import * as SpeechInput from "$lib/registry/ui/speech-input";
	import type { TranscriptionAdapter } from "$lib/registry/ui/speech-input";
 
	const adapter: TranscriptionAdapter = createMyAdapter(/* ... */);
	let value = $state("");
</script>
 
<div class="flex items-center gap-2">
	<input bind:value class="flex-1 rounded border px-3 py-2" />
	<SpeechInput.Root {adapter} onStop={(data) => (value = `${value} ${data.transcript}`.trim())}>
		<SpeechInput.RecordButton />
		<SpeechInput.Preview />
		<SpeechInput.CancelButton />
	</SpeechInput.Root>
</div>

Reversed Layout

Child order is the layout order — put the cancel button first if you want it to lead.

<SpeechInput.Root {adapter}>
	<SpeechInput.CancelButton />
	<SpeechInput.Preview />
	<SpeechInput.RecordButton />
</SpeechInput.Root>

Minimal (Record Button Only)

Drop the preview and cancel slots for an icon-only recorder; the transcript is still delivered via onStop.

<SpeechInput.Root {adapter} onStop={(data) => console.log(data.transcript)}>
	<SpeechInput.RecordButton />
</SpeechInput.Root>

Custom Placeholder

SpeechInputPreview shows its placeholder text until the first partial transcript arrives.

<SpeechInput.Root {adapter}>
	<SpeechInput.RecordButton />
	<SpeechInput.Preview placeholder="Say something..." />
	<SpeechInput.CancelButton />
</SpeechInput.Root>

Using the Hook

useSpeechInput() reads the context set up by SpeechInput.Root, so child components can render their own UI against the shared state.

<script lang="ts">
	import { useSpeechInput } from "$lib/registry/ui/speech-input";
 
	const state = useSpeechInput();
</script>
 
<p>
	Status: {state.error
		? `Error: ${state.error}`
		: state.isConnecting
			? "Connecting"
			: state.isConnected
				? "Recording"
				: "Idle"}
</p>
<p>Transcript: {state.transcript}</p>

API Reference

Prop Type Default Description
adapter TranscriptionAdapter — STT backend bridge that owns the transcription session. Conforms to TranscriptionAdapter.
size? ButtonSize "default" Shared size applied to SpeechInputRecordButton and SpeechInputCancelButton via context.
onStart? (data: SpeechInputData) => void — Fired once the adapter reports the connection is ready for audio.
onStop? (data: SpeechInputData) => void — Fired when the user stops recording. Receives a snapshot of the transcript — any in-flight partial is preserved.
onCancel? (data: SpeechInputData) => void — Fired when the user cancels recording. Receives the snapshot taken before partial + committed state is cleared.
onChange? (data: SpeechInputData) => void — Fired on every partial or committed transcript update.
onError? (error: Error) => void — Fired when the adapter surfaces an error or start() rejects.
children? Snippet — Compound children — typically SpeechInputRecordButton, SpeechInputPreview, and SpeechInputCancelButton in any order.
ref? HTMLDivElement | null $bindable(null) Bindable ref to the root &lt;div&gt; element.

Notes

  • The component is a compound primitive — SpeechInput.Root wires context and the adapter; SpeechInputRecordButton, SpeechInputPreview, and SpeechInputCancelButton read that context. Sub-components must be rendered inside a root or their useSpeechInput() call will throw.
  • The adapter reference is stored as a plain private field (not reactive) — swapping adapters mid-recording is unsupported and takes effect on the next start().
  • An internal request-id invalidates late callbacks, so a stop() or cancel() during connection reliably wins even if the adapter's onConnect fires after.
  • stop() preserves the in-flight partial transcript and forwards it to onStop; cancel() clears partial + committed state before firing onCancel. Choose based on whether the user is committing or discarding the utterance.
  • Teardown uses onDestroy rather than $effect cleanup so dev-mode HMR and parent re-renders do not cancel an active recording.
  • The record button is disabled while state.isConnecting; the cancel button is inert unless state.isConnected.