More Animations Snippets
Voice Assistant Orb — Free HTML CSS JS Snippet
Voice Assistant Orb · Animations · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Voice Assistant Orb — Animated AI Voice UI with Listening, Thinking, and Speaking States

The glowing orb has become the universal visual language for voice AI — Siri, ChatGPT voice mode, Gemini Live, and every AI hardware startup use a soft, living blob of colour that breathes while idle, pulses while listening, and dances while speaking. This component recreates that interface in HTML, CSS, and vanilla JavaScript: a layered gradient orb with a four-state machine (idle → listening → thinking → speaking), expanding pulse rings, an equalizer overlay, and a typewriter transcript of the assistant's reply.
Building the living orb from three blobs
The orb's organic look comes from three absolutely positioned radial-gradient circles — indigo, cyan, and fuchsia — each blurred 14px and combined with mix-blend-mode: screen so overlapping regions add up to bright, shifting hues rather than muddy overlaps. Each blob runs the same vaoDrift keyframe (translate + scale, alternate direction) but at different durations (6s, 7.5s, 9s) with negative animation-delays, so they start mid-cycle and never synchronise. Three out-of-phase loops are enough to make the colour field look continuously random — the same phase-offset trick behind lava-lamp backgrounds like the aurora bg.
The state machine
All behaviour hangs off a single state string and a setState() function that toggles .listening and .speaking classes and updates the status label. Tapping the orb from idle starts listening; the demo then simulates a conversation — 2.6 seconds of listening, 1.1 seconds of thinking, then a typed reply — using setTimeouts collected into a timers array. Every state change first calls clearTimers(), so tapping the orb mid-flow cancels the pending transitions cleanly instead of letting a stale timeout yank the UI back to a dead state. This cancel-on-transition discipline is exactly what you need when wiring real async APIs.
Listening: pulse rings and breathing
In the listening state the orb scales gently between 1 and 1.08 (vaoBreathe), while two border-only rings expand from scale(0.78) to 1.25 and fade out (vaoPulse), the second delayed 0.4s so the pulses alternate — the sonar effect users read as "it can hear me". The rings are pure borders with no fill, so they cost almost nothing to composite, and both animations use only transform and opacity, keeping the whole state GPU-friendly.
Speaking: equalizer and accelerated churn
The speaking state reveals a five-bar equalizer centred on the orb — white bars animating height between 8px and 34px with cascading 0.12s delays — and shortens the blobs' drift duration to 2.4s so the colours churn faster, as if energised by the voice. Meanwhile typeReply() types the assistant's answer character by character (24ms per tick) with a blinking caret built from a step-end opacity keyframe, the same technique as the typewriter effect. The typing loop checks state !== 'speaking' on every tick, so interrupting the assistant stops the text mid-word — matching how real voice UIs handle barge-in.
Connecting real speech APIs
The demo timeouts map one-to-one onto real events: start listening when you call SpeechRecognition.start() (see the speech to text snippet), move to thinking when the final transcript fires and you send it to your LLM, and enter speaking when the response streams back — feeding typeReply() from streamed tokens instead of a fixed string. For output audio, trigger speechSynthesis.speak() alongside the speaking state.
Customisation
Swap the three blob gradients to re-brand the orb, tune drift durations for calmer or wilder motion, resize via the .vao-orb width/height (everything inside is proportional), and replace the demo reply with your streaming response. The label strings live in setState() for easy localisation.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing the timer bookkeeping by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why clearTimers() runs at the very start of every click handler, and what specific bug would appear if a user tapped the orb mid-sequence without that cancellation in place. It's also worth asking how the three blob animations' different durations and negative delays combine to guarantee the composite color field never visibly repeats. For extending it, have it wire the demo timeouts to a real SpeechRecognition instance and a streaming LLM response instead of the fixed REPLY string, add a visual error state for failed recognition, or make the equalizer bar heights react to actual audio amplitude via an AnalyserNode instead of a fixed keyframe. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a Siri-style animated voice assistant orb in plain HTML, CSS, and vanilla JavaScript with no libraries, driven by a four-state state machine.
Requirements:
- A circular orb built from three overlapping radial-gradient blob layers in different hues, each blurred and set to mix-blend-mode: screen so overlapping colors brighten additively rather than muddying together. Each blob must run the same drift keyframe (translate plus scale, alternating direction) but at a different animation-duration and a different negative animation-delay, so the three loops never fall into a visibly synchronized, repeating pattern.
- Exactly four states: idle, listening, thinking, and speaking, all driven through a single setState function that toggles CSS classes on the orb and updates a status label's text — no state-specific logic scattered elsewhere.
- In the listening state, the orb must gently scale up and down (a breathing animation) while two concentric ring elements expand outward from a smaller starting scale and fade to transparent on a loop, with the second ring's animation delayed relative to the first so the pulses appear staggered rather than simultaneous.
- In the speaking state, a row of several vertical bars must appear centered on the orb, each animating its height between a short and tall value with a small cascading delay per bar to create an equalizer effect, and the blob drift animations must temporarily run at a noticeably faster duration than their idle speed.
- Clicking the orb from idle must start a simulated conversation sequence (listening for a few seconds, then thinking, then speaking a canned reply typed out character by character with a blinking caret) using chained setTimeout calls; every setTimeout handle must be tracked in an array.
- Clicking the orb again at any point during that sequence must immediately clear every tracked pending timeout and any in-progress typing must stop mid-character, then return the orb to idle — implement this cancellation so no previously scheduled state transition can fire after the interruption.Step by step
How to Use
- 1Paste the HTML, CSS, and JSA glowing orb of drifting indigo, cyan, and fuchsia blobs renders on a dark stage with a "Tap the orb to start listening" prompt.
- 2Tap the orbIt enters the listening state — the orb breathes and two sonar rings pulse outward alternately while the label switches to "Listening…".
- 3Watch the conversation flowAfter 2.6s it moves to "Thinking…", then to speaking — a five-bar equalizer appears and the reply types out with a blinking caret.
- 4Interrupt itTap the orb during any state to cancel — all pending timers clear and the orb settles back to idle, mid-sentence if it was speaking.
- 5Wire real speech APIsMap the states onto SpeechRecognition events and your LLM stream: listening on start(), thinking on final transcript, speaking as tokens arrive.
- 6Re-brand the orbSwap the three radial-gradient colours and drift durations; the size scales from the single .vao-orb dimension.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each blob is a blurred radial-gradient in one hue, and mix-blend-mode: screen makes overlaps additive — indigo over cyan brightens toward white-blue instead of darkening. All three run the same drift keyframe but at 6s, 7.5s, and 9s with negative delays, so their phases never align and the composite never visibly repeats. Changing any one duration changes the whole orb's character.
The demo chains listening → thinking → speaking with nested timeouts. If the user taps the orb mid-chain, a forgotten timeout would still fire later and drag the UI into a wrong state. clearTimers() runs at the start of every click, cancelling everything pending, and the typewriter additionally checks state !== 'speaking' each tick. This cancel-on-transition pattern is essential once real async APIs replace the timeouts.
On tap, call recognition.start() (webkitSpeechRecognition) and setState('listening'). In onresult with a final transcript, setState('thinking') and POST the text to your model. As the response streams, setState('speaking') and append tokens to the transcript element instead of running the fixed-string typewriter; optionally speak them with speechSynthesis. Errors and onend map to setState('idle').
No — almost everything animates transform and opacity, which the compositor handles on the GPU without layout or paint. The blurs are static filters applied once, not animated. The one exception is the equalizer bars animating height, but they are five 6px elements, so relayout cost is negligible. On low-power devices you can honour prefers-reduced-motion by pausing the drift and pulse animations in a media query.
Model state ('idle' | 'listening' | 'thinking' | 'speaking') in useState / ref() / a component field and derive the CSS classes from it in the template — the setState function dissolves into declarative bindings. Timers and speech-API subscriptions belong in useEffect / onMounted / ngOnInit with cleanup that clears them on unmount and on state change. The blob, ring, and equalizer keyframes are pure CSS and port unchanged.