Other Frameworks
The package is called @voxide/react because the widget is written in React. The client underneath it never was. Since version 0.8.0 that client ships on its own, so Vue, Svelte, Angular, Solid, Django, Rails, Laravel, a Go template, plain TypeScript or a single HTML file can run the same agent, register the same capabilities, and bind the same live state.
#Picking a path
There are three ways in. They are the same client with different amounts of packaging around it, and the thing that separates them is whether you get a user interface or write one.
1. The React package. npm install @voxide/react, then <VoxideWidget client={ai} />. React 18 or newer, which includes Next.js. This is the only path with a prebuilt interface: the chat panel, the voice bar, the voice orb and the WebGPU visualizer. Take it if you can. See the quick start.
2. The core import. import { VoxideClient } from "@voxide/react/core". Any bundler, no React, no React in your dependency tree. This is the path for Vue, Svelte, Angular, Solid and plain TypeScript.
3. The script tag. One file from a CDN, no build step, no package manager. It defines a window.Voxide global. This is the path for server-rendered templates: Django, Rails, Laravel, Go and Gin, PHP, WordPress, htmx, Astro, Hugo, or a static HTML file you edit by hand. The bundle is 23.6 KB minified, 7.6 KB gzipped.
#The script tag
Paste the tag, create a client, register what the agent is allowed to do, and wire it to your own button. The example below is complete: it is the whole integration, not an excerpt.
<script src="https://unpkg.com/@voxide/react@0.8.0/dist/voxide.browser.js"></script> <button id="vox-talk">Talk to us</button><span id="vox-status">idle</span> <script> const ai = new Voxide.VoxideClient({ publicKey: "vox_pub_..." }); // 1. What the agent is allowed to do. The handler is your code, on your page. ai.register({ applyCoupon: { description: "Apply a discount code to the current order.", params: { code: { type: "string", required: true }, }, handler: async ({ code }) => { const res = await fetch("/cart/coupon", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); if (!res.ok) return { status: "error", message: "That code was rejected." }; const { total } = await res.json(); document.getElementById("order-total").textContent = total; return { status: "ok", total }; }, }, }); // 2. What the visitor is looking at. Read fresh every turn, so say "the cart" // and the agent knows which cart. ai.bindState(() => ({ page: document.title, itemsInCart: document.querySelectorAll(".cart-row").length, orderTotal: document.getElementById("order-total").textContent, })); // 3. Load the project config. Once, on page load. ai.init().catch((err) => console.error("Voxide init failed", err)); // 4. Your own UI. Nothing is drawn for you on this path. const button = document.getElementById("vox-talk"); const label = document.getElementById("vox-status"); button.addEventListener("click", () => { const { status } = ai.getSnapshot(); if (status === "idle" || status === "error") ai.connect(); else ai.disconnect(); }); // subscribe fires on every session change and returns an unsubscribe function. ai.subscribe(() => { const snap = ai.getSnapshot(); label.textContent = snap.status; button.textContent = snap.status === "idle" ? "Talk to us" : "Stop"; });</script>getSnapshot() is synchronous and cheap, and status is one of idle, armed, connecting, listening, thinking, speaking, executing and error. The snapshot also carries messages, which is the transcript, so a chat log is a loop over an array rather than a subscription of its own.
Pin the version in the URL, as above. @latest on a CDN means your site changes when we publish, which is not a thing you want to find out about from a customer.
The client code above is the same everywhere. What differs is which template the tag belongs in, and the answer is always the layout every page already extends, so the agent survives navigation instead of being torn down on each request.
In the base template every other template extends, just before {% endblock %}.
{# templates/base.html #}<body> {% block content %}{% endblock %} <script src="https://unpkg.com/@voxide/react@0.8.0/dist/voxide.browser.js"></script> <script src="{% static 'js/voxide.js' %}"></script></body>#The core import
If you already have a bundler, import from @voxide/react/core instead of the package root. Same client, same methods, and React is not in the module graph, so it is not in your build either. The types come with it.
// voxide.ts: one module, imported wherever you need the agent.import { VoxideClient } from "@voxide/react/core";import { cart } from "./store"; export const ai = new VoxideClient({ publicKey: "vox_pub_..." }); ai.register({ addToCart: { description: "Add an item to the shopping cart.", params: { itemId: { type: "string", required: true }, qty: { type: "number" }, }, handler: async ({ itemId, qty = 1 }) => { await cart.add(itemId, qty); return { status: "ok" }; }, },}); // Your framework's store, read on demand rather than pushed on change.ai.bindState(() => ({ items: cart.items, total: cart.total })); await ai.init();From there, wire it to whatever your framework calls reactivity. In Vue that is a ref updated from the subscription, in Svelte a store, in Angular a signal. The client does not care which:
// Anything that re-renders when you tell it to.const unsubscribe = ai.subscribe(() => setSnapshot(ai.getSnapshot())); // Driving a session by hand.await ai.connect(); // opens the mic and the socketawait ai.sendText("where is my order"); // typed input, same sessionai.interrupt(); // barge-in: stop the agent mid-sentenceai.disconnect(); // Levels for your own meter or visualiser. Not reactive: poll them from// requestAnimationFrame, they will not change under a subscription.ai.getInputLevel(); // 0 to 1, the visitor's microphoneai.getOutputLevel(); // 0 to 1, the agent's own speech // Wake word, if your plan has it. Feature-detect first: Firefox cannot.if (ai.isWakeWordAvailable()) ai.armWakeWord();ai.disarmWakeWord(); unsubscribe();Where you call init is the only part that differs by framework. Put it in whatever runs once for the whole app, not per route, or a call in progress ends the moment someone navigates.
In a root-level component, or a plugin if you prefer.
<!-- App.vue --><script setup>import { onMounted, onUnmounted, ref } from "vue";import { ai } from "./voxide"; const status = ref("idle");let stop; onMounted(async () => { stop = ai.subscribe(() => { status.value = ai.getSnapshot().status; }); await ai.init();});onUnmounted(() => stop?.());</script> <template> <button @click="ai.connect()">Talk to us</button> <span>{{ status }}</span></template>The rest of the client is unchanged on every path: ai.on(event, cb) for one-off event handlers, ai.setUser() to tell the agent who is signed in, ai.setActiveRoute() to keep it in step with your router, ai.use(mw) for middleware, ai.enableNavigation() for a built-in navigate capability, and ai.onConfirmation() to intercept anything marked dangerous. These are the public names as of 0.8.0. If a snippet you found anywhere calls a method starting with an underscore, it predates this release and you should not copy it.
#What you do not get without React
Everything the widget draws. The chat panel, the voice bar, the voice orb, the launcher, the transcript view and the WebGPU orb are React components in the package root, and none of them are reachable from the core or the browser build. The dashboard's Appearance tab configures those components, so on paths 2 and 3 most of it has nothing to render against.
What you do get is the part that is hard: the WebSocket session, the audio capture and playback, barge-in, tool dispatch, confirmation for dangerous actions, state binding, transcripts, redaction and handoff. Everything configured at the agent level in the dashboard, including the system prompt, the model, the knowledge base and the usage limits, applies the same way. What is left for you is markup and CSS: a button, a status indicator, and a list of messages.
Budget for that honestly. A usable panel is an afternoon, not ten minutes, and audio permission prompts and error states are most of the fiddly part. If your app is React and you are choosing a path on principle, take the widget.
#Keys and domains
Identical on all three paths. The vox_pub_… key is publishable and belongs in page source, including a Django template a crawler can read. What makes it safe is the domain whitelist, not secrecy: add your production origin under project → Settings, and localhost always works. A key used from an origin you have not whitelisted is refused at the connection, whether it arrived through a bundler or a script tag. See Security & CORS.
