Documentation

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.
  • async is safe. The loader initializes on DOMContentLoaded, 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.com does not cover www.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.

A Hiroi assistant embedded on a web page as a floating orb in the bottom corner

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.

The Minimal presentation — an ambient assistant pill on a live page

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:

  1. Built-in defaults — the values in the tables above.
  2. Your data- attributes — applied when the widget is constructed.
  3. 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-mode
  • data-show-waves
  • data-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.html is enough for every route.
  • If you inject it from a component, guard the injection with an id check 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

  1. Go to Appearance → Theme Editor, or use a plugin such as Insert Headers and Footers.
  2. Open your theme's footer.php file.
  3. Paste the script tag just before the closing </body> tag.
  4. 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

  1. In your Shopify admin, go to Online Store → Themes.
  2. Click Actions → Edit Code on your active theme.
  3. Open theme.liquid.
  4. Paste the script tag just before the closing </body> tag.
  5. 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

  1. In your Squarespace dashboard, go to Settings → Advanced → Code Injection.
  2. Paste the script tag into the Footer field.
  3. Click Save.
  4. Add your Squarespace domain (for example yoursite.squarespace.com) to the Allowed Domains list.

Wix

  1. In your Wix dashboard, go to Settings → Custom Code (under Advanced).
  2. Click Add Custom Code.
  3. Paste the script tag, set placement to Body - End, and apply to All Pages.
  4. Click Apply.
  5. 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