Embedding the widget
Everything needed to put a Hiroi assistant on a page: the script tag, every data- attribute the
loader reads, how those attributes interact with your dashboard settings, and the placement details
for the frameworks and site builders people actually use.
The embed code
One script tag, anywhere in the page, is the whole integration:
<script async src="https://hiroi.ai/static/va-wave-widget.js"
data-site-id="YOUR_SITE_ID"></script>
Your site ID is already filled in for you on the Deploy tab of the assistant editor — copy the block from Embed Code. See Deploying your assistant.
Placement notes:
- Put it just before the closing
</body>tag. The widget builds its own element and appends it to<body>, so it does not matter where in the document the tag sits, but the end of the body keeps it out of the critical rendering path. asyncis safe. The loader initializes onDOMContentLoaded, or immediately if the document has already finished parsing, so an async or deferred load still starts the widget.- Add the page's domain to Allowed Domains on the Deploy tab, or the widget will not
authenticate and nothing renders. Matching is exact —
example.comdoes not coverwww.example.com.
Use the va-wave-widget.js file name
The loader locates its own tag by looking for a script whose src contains va-wave-widget.
If you rename the file, proxy it under a different name, or copy the bundle to
/js/chat.js, the script downloads and then silently does nothing. Always point src at
/static/va-wave-widget.js.
Nothing is drawn until the server confirms the site. If authentication fails — wrong site ID, domain not on the list, expired session token — the widget stops and leaves the page untouched.

The two presentations
The same embed produces one of two presentations, chosen by the Display Mode setting on the
Appearance tab or by the data-display-mode attribute:
| Presentation | data-display-mode |
What the visitor sees |
|---|---|---|
| Widget | widget (default) |
A floating orb in a corner of the page, with the wave skin unless waves are turned off. |
| Minimal | fullscreen |
An ambient pill with a settings gear that rides your live page; the page stays visible and clickable. An optional waves band can run along the bottom. |

Minimal needs room. On viewports narrower than 640px the widget switches itself back to the corner orb, and re-evaluates that decision when the device is rotated.
Attribute reference
Every attribute the loader reads. All are optional except the credential (data-site-id or
data-session-token) — with neither, the widget does not start.
Identity and connection
| Attribute | Values | Default | Notes |
|---|---|---|---|
data-site-id |
Your assistant's site ID | — | Required in Allowed Websites mode. Safe to publish; the server checks the request's Origin against your allowed domains. |
data-session-token |
A signed token minted by your backend | — | Used instead of data-site-id in Session Signed mode. See Widget authentication. |
data-base-url |
An absolute origin | The origin of the script src |
Only needed when the page loads the bundle from a different host than the API it should talk to. |
data-debug |
true |
Off | Turns on console logging from the widget. Logging is on automatically on localhost and 127.0.0.1. |
Presentation
| Attribute | Values | Default | Notes |
|---|---|---|---|
data-display-mode |
widget, fullscreen |
widget |
Present on the tag = pinned; the dashboard setting is ignored. |
data-show-waves |
true, false |
true |
false renders a plain orb with no wave animation. Present on the tag = pinned. |
data-fullscreen-show-waves |
true, false |
false |
The bottom waves band in Minimal. Present on the tag = pinned. |
data-fullscreen-panel-side |
left, right |
right |
Which side the Minimal chat panel docks to. |
data-position |
bottom-right, bottom-left, top-right, top-left |
bottom-right |
Corner the orb sits in. |
data-size |
A number of pixels | 72 |
Orb diameter. The dashboard slider offers 50–120. |
Appearance
| Attribute | Values | Default | Notes |
|---|---|---|---|
data-primary-color |
CSS color | #8b5cf6 |
Voice icon, buttons, focus rings. |
data-secondary-color |
CSS color | #6d28d9 |
|
data-ring-color |
CSS color | Unset | The orb's rim and highlight. Setting it switches the orb to an explicit palette. |
data-idle-color |
CSS color | The primary color | Wave color while idle. |
data-listening-color |
CSS color | #22c55e |
Wave and orb color while listening. |
data-speaking-color |
CSS color | #3b82f6 |
Wave and orb color while speaking. Also colors links and blockquotes inside message content. |
data-processing-color |
CSS color | #f59e0b |
|
data-wave-speed |
A number | 1.0 |
Multiplier. |
data-wave-height |
A number | 1.0 |
Multiplier. |
data-glow-intensity |
0–1 |
0.4 |
|
data-bot-name |
Text | Empty | Name shown beside the orb. |
data-logo-url |
An image URL | None | |
data-always-show-logo |
true |
Off | Keeps the logo visible instead of showing it only at rest. |
The chat panel's surface and text colors are not configurable from the embed. The panel follows
the visitor's light or dark theme so it stays readable on any page; data-bg-color and
data-text-color are still parsed for backwards compatibility but no longer change it. To restyle
the widget beyond these attributes, see Theming and custom CSS.
Content
| Attribute | Values | Default | Notes |
|---|---|---|---|
data-lang |
A locale tag such as en-US |
en-US |
Parsed, then overridden by the assistant's configured language. It does not configure speech recognition — the widget sends the browser's own navigator.language as browser_locale on every chat and voice request. |
data-dynamic-variables |
A JSON object, as a string | None | Per-visitor context. See below. |
Boolean attributes are compared as strings. data-always-show-logo, data-fullscreen-show-waves
and data-debug only take effect when the value is exactly true; data-show-waves is off only
when the value is exactly false.
How settings are decided
Three layers, in order:
- Built-in defaults — the values in the tables above.
- Your
data-attributes — applied when the widget is constructed. - Your dashboard settings — fetched from the server the moment the widget starts, and applied over the top of everything else.
So for almost every attribute, the dashboard wins whenever the corresponding setting has a value.
data-position gives way to Position on the Appearance tab, data-bot-name gives way to
the assistant's name, data-lang gives way to the assistant's configured language, and so on. An
attribute only survives when the matching dashboard setting is unset — treat attributes as a seed,
not an override.
Three attributes break that rule, because they change the shape of the embed rather than its styling. If the attribute is present on the tag at all, its value is pinned and the server setting is ignored for that page:
data-display-modedata-show-wavesdata-fullscreen-show-waves
That is what lets one assistant appear as a corner orb on your marketing site and as the Minimal pill inside your app, without either page fighting the dashboard.
Tip
Because pinning is triggered by the attribute's presence, writing
data-display-mode="widget" is meaningful — it locks the page to the orb even if someone later
switches the assistant to Minimal in the dashboard.
Passing visitor context
If your page already knows who the visitor is, hand that to the assistant with
data-dynamic-variables. The value is a JSON object, serialized into the attribute:
<script async src="https://hiroi.ai/static/va-wave-widget.js"
data-site-id="YOUR_SITE_ID"
data-dynamic-variables='{"user_name":"Dana Reyes","user_email":"dana@example.com","plan":"pro"}'></script>
The object is sent with every chat and voice request and becomes a User Context section in the assistant's instructions, so it can greet the visitor by name or reason about their account. Up to 50 entries are used; values are converted to text and sanitized before they reach the model. Invalid JSON is ignored.
Do not put anything secret in it — the attribute is plain HTML that any visitor can read.
Single-page apps
The widget mounts itself onto <body>, outside your framework's root element, so it is unaffected
by client-side rendering. It also watches pushState, replaceState and popstate, so it notices
route changes on its own and tells the assistant when the visitor moves to a new page.
That leads to one rule: inject the script once, for the lifetime of the app. Do not add or remove it per route.
- Mounting the tag in your root layout or
index.htmlis enough for every route. - If you inject it from a component, guard the injection with an
idcheck so a remount does not add a second tag. - Injecting a second tag re-initializes the widget and tears down the previous instance, which drops the open conversation and any audio in flight.
- If you genuinely need to remove it — for example when a visitor signs out — call
window.VAWaveWidget.destroy(). See JavaScript API.
Framework and platform guides
Only the placement differs. The tag itself is identical everywhere.
Plain HTML
Before the closing </body> tag of every page, or in your shared footer include:
<script async src="https://hiroi.ai/static/va-wave-widget.js"
data-site-id="YOUR_SITE_ID"></script>
</body>
</html>
React
Inject once from a component rendered at the top of the tree, and guard against remounts:
import { useEffect } from 'react';
export function HiroiWidget() {
useEffect(() => {
if (document.getElementById('hiroi-widget')) return;
const s = document.createElement('script');
s.id = 'hiroi-widget';
s.src = 'https://hiroi.ai/static/va-wave-widget.js';
s.async = true;
s.dataset.siteId = 'YOUR_SITE_ID';
document.body.appendChild(s);
}, []);
return null;
}
s.dataset.siteId produces the data-site-id attribute. Render <HiroiWidget /> once, next to
your router — not inside a route component.
Next.js
Use next/script in the root layout so the tag survives navigation:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://hiroi.ai/static/va-wave-widget.js"
strategy="afterInteractive"
data-site-id="YOUR_SITE_ID"
/>
</body>
</html>
);
}
For the pages directory, put the same <Script> in pages/_app.js. Either way it mounts once and
stays mounted across client-side navigation.
Vue
The widget does not need to be a component. Add the tag to index.html, after the mount point:
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script async src="https://hiroi.ai/static/va-wave-widget.js"
data-site-id="YOUR_SITE_ID"></script>
</body>
If your build injects the entry script for you, the same tag can go in the index.html template
unchanged; nothing about it depends on Vue's lifecycle.
WordPress
- Go to Appearance → Theme Editor, or use a plugin such as Insert Headers and Footers.
- Open your theme's
footer.phpfile. - Paste the script tag just before the closing
</body>tag. - Add your WordPress domain to the Allowed Domains list on the Deploy tab.
The WPCode plugin is the easiest way to add scripts without editing theme files, and it survives theme updates.
Shopify
- In your Shopify admin, go to Online Store → Themes.
- Click Actions → Edit Code on your active theme.
- Open
theme.liquid. - Paste the script tag just before the closing
</body>tag. - Add your Shopify domain to the Allowed Domains list.
If you use a custom domain as well as yourstore.myshopify.com, add both — matching is exact.
Squarespace
- In your Squarespace dashboard, go to Settings → Advanced → Code Injection.
- Paste the script tag into the Footer field.
- Click Save.
- Add your Squarespace domain (for example
yoursite.squarespace.com) to the Allowed Domains list.
Wix
- In your Wix dashboard, go to Settings → Custom Code (under Advanced).
- Click Add Custom Code.
- Paste the script tag, set placement to Body - End, and apply to All Pages.
- Click Apply.
- Add your Wix domain to the Allowed Domains list.
Checking the embed
Use Test Widget on the Deploy tab to open a test page with your assistant on it — no embedding required. It shows the last saved version, so save first if you have just changed settings.
If the widget does not appear on your own page, work through these in order:
| Symptom | Cause to check |
|---|---|
| Nothing at all, no network request to Hiroi | The script src does not contain va-wave-widget, or the tag never made it into the page. |
| The bundle loads but nothing renders | The page's domain is not in Allowed Domains, or neither data-site-id nor data-session-token is set. |
| Works locally, not in production | localhost:5566 is on the list but the live domain is not. Add every host, including the www. variant. |
| Widget appears twice, or a conversation resets on navigation | The script is being injected per route. Inject once. |
Add data-debug="true" to the tag to see the widget's own log lines in the browser console while
you diagnose.
Next steps
- Widget authentication — Allowed Websites versus Session Signed, and minting tokens.
- JavaScript API — controlling the widget from your page.
- Page tools — letting the assistant act on the page it is embedded in.
- Theming and custom CSS — styling beyond the
data-attributes.