@microsoft/voice-widget-ui
v0.2.0
Published
Supported UI (Preact shell) for the Voice Agent Widget SDK. Powers the first-class <voice-agent> one-line embed; customers who want full brand control can instead build their own UI on the headless @microsoft/voice-widget core.
Readme
@microsoft/voice-widget-ui
The supported UI for the Voice Agent Widget SDK — a Preact, Shadow-DOM shell (trigger, expandable
panel, status/turn states, orb, mute) plus the mountVoiceAgent orchestrator that binds it to a
provider and a session. This is the UI behind the first-class one-line <voice-agent> embed.
Want full brand control instead? Build your own UI on the headless
@microsoft/voice-widget core (createVoiceAgent) — the advanced escape hatch.
Install & use
import { mountVoiceAgent } from "@microsoft/voice-widget-ui";
import "@microsoft/voice-widget-provider-voicelive"; // self-registers the "voicelive" provider
const controller = mountVoiceAgent(document.getElementById("assistant")!, {
provider: "voicelive",
config: { targetType: "model", model: "gpt-realtime" },
authEndpoint: "https://your-broker.example.com/session", // provider-specific session grant
ui: {
variant: "compact",
placement: "bottom-right",
showTranscript: true,
textInput: true,
},
});
// Session: controller.start() / stop() / destroy() / getState()
// Typed turn: controller.sendText("Hello")
// Live UI: controller.setLangStrings() / setTheme() / setAccentColor()For the declarative one-liner (<script> + <voice-agent>), use
@microsoft/voice-widget-embed, which bundles this package.
This package bundles no provider. Any registered one works — swap the import and the provider
name. See Writing a provider.
createWidgetShell(target, opts) is also exported for advanced hosts that want the UI controller
without the session orchestration.
ui options
| Option | Meaning |
| --- | --- |
| variant | bar / compact / full |
| placement | bottom-right / bottom-left / top-right / top-left / inline |
| startLabel | nonblank trigger text; blank values use the built-in default |
| defaultExpanded | start expanded |
| muteButton | show the mic mute button — opt-in, off by default |
| showTranscript | render the live transcript — opt-in, off by default |
| textInput | render the typed-message form — opt-in, off by default; enabled while connected |
| theme | auto (default — follows the OS via prefers-color-scheme) / light / dark |
| accentColor | any CSS color for the accent; sets both accent stops to that color for a solid fill. Falls back to the brand default if omitted/invalid |
| langStrings | Partial<Strings> i18n overrides merged over the built-in English strings (see Internationalization) |
Transcript and typed messages
The built-in transcript and text form are independent opt-ins. Transcript deltas are accumulated into the current agent turn and replaced by the final transcript when it arrives. The shell keeps the latest 100 messages, preserves them when a call stops, and clears them when a new call starts.
const controller = mountVoiceAgent(el, {
/* session options */
ui: { showTranscript: true, textInput: true },
onTranscript: (message) => console.log(message),
});
await controller.start();
controller.sendText("Can you summarize my order?");sendText accepts nonblank text only while connected and requires a provider that advertises
supportsText. Voice Live and the Foundry provider route typed turns through the
broker-held Realtime control WebSocket used by the WebRTC session. A typed user turn is emitted
through onTranscript immediately because runtimes do not necessarily echo typed input as an
audio-transcription event.
Lifecycle and fresh start
The shell surfaces terminal disconnected and error states and changes the call control back to
Start a call. Starting again clears the previous transcript and performs a brand-new broker/SDP
handshake. Stopping or losing a Voice Live session releases the peer connection, media tracks,
audio/level resources, and broker control session. It does not resume the previous conversation.
Theming
The shell is themed through CSS custom properties set on the host element, plus data-* state
attributes you can target. These variables and attributes are the supported theming API — the
internal .va-* class names are not and may change in any release.
| CSS variable | Default | Role |
| --- | --- | --- |
| --va-accent | #6d28d9 | Primary accent (trigger, call button) |
| --va-accent-2 | #8b5cf6 | Accent gradient stop |
| --va-on-accent | #ffffff | Foreground on accent surfaces |
| --va-surface | #ffffff | Panel / card background |
| --va-fg | #1a1a2e | Primary text |
| --va-muted-fg | #6b7280 | Secondary text |
| --va-muted | #f3f4f6 | Muted control background |
| --va-border | rgba(17, 24, 39, 0.08) | Borders |
| --va-error | #ef4444 | Error state |
| --va-danger | #e11d48 | Destructive (hang-up / active mute) |
--va-level (0..1) is set at runtime from the agent output level to drive the visualizer — read-only,
do not set it. State attributes on the host: data-placement, data-variant, data-status,
data-expanded, data-muted, data-reduced-motion, data-theme.
Light / dark / auto
Set the theme option (auto | light | dark, default auto). The shell resolves it to
data-theme="light" or data-theme="dark" on the host and sets the CSS color-scheme accordingly;
auto follows the OS prefers-color-scheme and updates live. The dark palette overrides the
surface/text/border variables under :host([data-theme="dark"]) — accent, error and danger stay
shared. Override any --va-* variable yourself to go further, or set accentColor (a shortcut that
writes the same value to --va-accent and --va-accent-2) for a solid accent fill:
mountVoiceAgent(el, { /* …session… */, ui: { theme: "dark", accentColor: "#0f6cbd" } });Update either value later without remounting the widget or interrupting an active session:
controller.setTheme("light");
controller.setAccentColor("#2899f5");
controller.setAccentColor(undefined); // restore the CSS-variable/default paletteInternationalization
All UI strings are externalized and English ships as the reference bundle. Supply your own strings
via the langStrings option (or the lang-strings attribute on the <voice-agent> embed) — a flat
map merged over the built-in English defaults; unknown keys are ignored and any key you omit falls
back to English. A nonblank startLabel still wins the trigger/idle text if both are set.
mountVoiceAgent(el, {
/* …session… */,
ui: { langStrings: { idle: "Habla con el asistente", startCall: "Iniciar llamada", endCall: "Colgar" } },
});Keys: idle, connecting, listening, thinking, speaking, connected, disconnected,
error, startCall, endCall, expandAssistant, minimize, mute, unmute, panelLabel,
transcriptLabel, userLabel, agentLabel, textInputPlaceholder, sendMessage.
We do not ship first-party translated locale bundles during preview — bring your own translations.
Reacting to your site's locale
The widget does not detect the language itself — your app selects it and passes the matching strings in. Two common approaches:
Pick a bundle by locale. Keep your own per-locale maps and choose one at mount:
const STRINGS: Record<string, Partial<Strings>> = {
"zh-Hans": { idle: "与助手对话", startCall: "开始通话", endCall: "挂断" },
"ja": { idle: "アシスタントに話す", startCall: "通話を開始", endCall: "終了" },
};
const locale = document.documentElement.lang; // or your i18n library's active locale
mountVoiceAgent(el, { /* …session… */, ui: { langStrings: STRINGS[locale] } });Only override the keys you've translated — omitted keys fall back to English, so a partial translation still renders cleanly.
Reuse your existing i18n library. If you already run i18next / FormatJS / etc., map the widget's
keys through your own t() so the widget's strings live alongside the rest of your UI:
const langStrings = { idle: t("voiceWidget.idle"), startCall: t("voiceWidget.startCall") /* … */ };
mountVoiceAgent(el, { /* …session… */, ui: { langStrings } });Live language switches. Replace the current override map without remounting or restarting an active session. Each call resolves from the built-in English defaults, so omitted keys do not retain values from the previous language:
controller.setLangStrings(STRINGS[nextLocale]);
controller.setLangStrings(undefined); // restore English defaultsFor the declarative embed, update the lang-strings attribute in place. The theme, accent-color,
and lang-strings attributes are observed live; see @microsoft/voice-widget-embed.
Language, direction, and strings in one call
setUiLocale updates the UI-locale metadata as a unit — language (lang), writing direction (dir),
and the string overrides — without remounting or restarting an active session. It is the surface a host
drives from its own i18n runtime when switching locale mid-call. Every field is independently optional,
so you can update strings alone, direction alone, or all three together:
controller.setUiLocale({
lang: "ar",
dir: "rtl",
strings: { idle: "تحدث إلى المساعد", endCall: "إنهاء المكالمة" },
});lang and dir are applied to the widget host element (.va-widget-host), so a screen reader
announces the new language and the panel lays out in the right direction — while the widget keeps
inheriting the page's ambient dir when you don't set one. dir accepts "ltr", "rtl", or
"auto"; passing an empty/undefined lang clears it. Strings re-resolve from the English defaults on
every call, so an omitted key never keeps the previous language's value. On the declarative embed the
equivalent surface is the ui-locale attribute (a JSON object) or the element's setUiLocale() method;
see @microsoft/voice-widget-embed.
Public API & stability
Stable (semver-guarded): the exports above, the ui options, the controller's sendText,
setLangStrings, setUiLocale, setTheme, and setAccentColor methods, the currently-read
<voice-agent> attributes, and the theming contract (--va-* variables + data-* attributes).
Internal (may change): .va-* class names, component internals, DOM structure.
Browser support
Evergreen browsers, last 2 stable major versions of Chrome, Edge, and Firefox; Safari 16+
(macOS); iOS Safari 16+; Android Chrome (last 2). Requires WebRTC (RTCPeerConnection),
AudioWorklet, Shadow DOM v1 / Custom Elements v1, and ES2020. No IE11 or legacy (EdgeHTML) Edge.
Versioning & support
The package follows semver. During public preview it stays 0.x: a minor may include breaking
changes, a patch never does; full semver is committed at 1.0 (GA). We fix a11y, i18n,
theming-contract, in-matrix browser-compat, and security issues in the shipped shell. Forks, BYO-UI on
the headless core, and customer-written providers are out of support scope.
