Voice input

● Stable · v0.1

Speak instead of type. A mic button that records, shows a live waveform while it listens, then a transcript once it's understood — built for an owner who'd rather say "twelve thousand kyat from Daw Hla" than tap it out.

In context

9:41

Try it

Tap the mic, watch the waveform and timer, then stop — it runs the record → transcribe → result cycle.

Every state

The flow has real moments — listening, thinking, done — and each one is shown plainly so the owner is never unsure whether it's still recording.

Purpose

Voice input lowers the cost of capture for a low-literacy owner. Typing an amount and a name is friction; saying it is not. It's the most inclusive way to get a sale into DayTwo — so it gets a big, obvious mic and unambiguous states.

Anatomy

  • Mic button — a large (72dp) circular target. The single most important control on the field.
  • Waveform + timer — while recording, jittering bars and a running m:ss prove it's listening; a stop button ends it.
  • Processing — a spinner with "Transcribing…" so the wait is accounted for.
  • Transcript — the recognised text in a quiet panel, with Record again to redo it.
  • Recovery — an error state with retry, and a distinct permission-denied state that explains how to turn the mic on.

Behaviour

Controlled by status: your app drives idle → recording → processing → done (or error / denied) and supplies duration, transcript, and error. The component fires onStart, onStop, onReset, and onRetry. Capturing audio and transcribing it is the app's job (e.g. expo-av plus a speech-to-text service) — this component is the interface around it. All on-screen words can be overridden for Burmese/Thai.

Code

import { VoiceInput } from '@daytwo/components'
const [status, setStatus] = useState('idle')
const [duration, setDuration] = useState(0)
<VoiceInput
label="Record the sale"
helperText="Say the amount and the customer's name"
status={status}
duration={duration}
maxDuration={60}
transcript={text}
onStart={beginRecording} // start expo-av + the timer
onStop={stopAndTranscribe} // -> 'processing' -> 'done'
onReset={() => setStatus('idle')}
onRetry={beginRecording}
/>

status is 'idle' | 'recording' | 'processing' | 'done' | 'error' | 'denied'.

When to use

  • To capture a short spoken value — an amount, a customer name, a quick note.
  • When the user may struggle to type, or has their hands full at the stall.
  • As a faster alternative beside a text Input, not its only replacement.

When NOT to use

  • For long-form or precise text where the owner must see and edit every character — pair it with an Input.
  • For exact numbers that must be confirmed — always show the transcript so it can be corrected before saving.
  • Where there's no transcription backend — without it, this is just an audio recorder.

Anti-patterns

Do
A big labelled mic with a prompt, and — once done — the transcript shown so the owner can check it before saving.
Don't
Recording with no waveform, timer, or transcript, so the owner can't tell if it heard them or what it captured.
Always show the transcript

Voice is only trustworthy if the owner can see what was heard. Show the transcript and let them re-record — never save a spoken amount blind.

Edge cases

Recording
Live waveform + running timer + a clear stop
WhyRemoves all doubt that it's listening
Max duration
Show a countdown in the last seconds, then auto-stop
WhyKeeps clips short and transcribable
Processing
Spinner with "Transcribing…"
WhyThe wait is named, not a frozen screen
Done
Show the transcript with Record again
WhyThe owner verifies before it's saved
Couldn't transcribe
Error message + retry
WhyA noisy market shouldn't be a dead end
Permission denied
A distinct state explaining how to enable the mic
WhyThe fix is in their hands, so name it
Disabled / offline
Grey the mic; fall back to typing
WhyVoice needs a backend — never trap the owner

Tokens used

color.surface.primarycolor.background.secondarycolor.brand.primarycolor.brand.onPrimarycolor.text.primarycolor.text.secondarycolor.text.tertiarycolor.border.subtlecolor.feedback.errorSolidcolor.feedback.errorFgcolor.feedback.errorBorderradius.lgradius.pilltouchTargets.mintypography.title3