What controls what
The widget is configured from two independent places. Knowing which is which saves a lot of guesswork:
| What you want to change | Where it lives |
|---|---|
| Which agent loads, where it mounts, whether it opens on load, whether the close button shows | data-* attributes on the <script> tag |
| Corner, colors, fonts, panel size, corner radius, labels, languages | The character’s appearance config — set in the dashboard, or served from your own URL via data-appearance-url |
In short: the script tag decides placement behaviour, the appearance config decides how it looks. There is no data-position or data-color attribute.
The base embed
Paste this before </body> on any page. Everything else on this page is a variation of it.
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot">
</script>data-agent is the only required attribute — it’s your character’s slug. If it’s missing the widget logs [voice-agent] data-agent (character slug) is required and stops.
The script tag can go anywhere in the page, but its position in the DOM matters for inline mode — see below. For the floating bubble it makes no difference.
Mode 1 — Floating bubble (default)
With no extra attributes the widget mounts fixed on <body>: a circular button in a screen corner that expands into the voice panel. It sits above your page at z-index: 2147483647 and is fully isolated in a shadow DOM, so it can’t inherit or leak your CSS.
The corner comes from the appearance config, not the script tag:
layout.position | Result |
|---|---|
"bottom-right" | Default. Bubble and panel anchored bottom-right. |
"bottom-left" | Bottom-left. |
"top-right" | Top-right. |
"top-left" | Top-left. |
Set it under Characters → [your character] → Appearance → Position, or in appearance JSON:
{ "layout": { "position": "bottom-left" } }Mode 2 — Inline, inside your own layout
Set data-inline="true" to drop the widget into your page as a normal block of content — a hero section, a sidebar card, a modal, a dedicated /talk-to-us page. The floating bubble is hidden and the panel fills the container you give it.
<div class="voice-embed">
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-inline="true">
</script>
</div>
<style>
.voice-embed {
position: relative; /* required — the panel is absolutely positioned */
height: 620px; /* required — the panel fills the container */
width: 100%;
max-width: 420px;
overflow: hidden;
border-radius: 16px; /* your container, your corners */
}
</style>Three rules for inline mode:
- The script tag must live inside the container. The widget mounts into the script’s parent element. If your CMS or bundler moves the tag (e.g. hoists it to
<head>), inline mounting falls back to<body>and you get a full-page panel. - The container needs
position: relativeand a real height. The panel isposition: absolute; inset: 0— with a zero-height or statically positioned parent, nothing is visible. - Inline implies open. There’s no bubble to click, so the panel opens on load automatically.
Inline mode ignores panel_width, panel_height, border_radius and layout.position from the appearance config — your container sets the size and corners, and the panel renders square-cornered with no shadow so it sits flush inside it.
Inline is the right choice for a “Talk to us” page or a product-tour section. Use the floating bubble when the agent should be reachable from every page without taking up layout space.
Open the panel on load
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-open-chat="true">
</script>The panel expands as soon as the widget boots, instead of waiting for a click on the bubble. Useful for landing pages and demos.
data-open-chat must be the literal string "true" or "1". A bare data-open-chat (no value) is not enough — unlike data-close-button-hide, which does accept the bare form.
Hide the close button
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-inline="true"
data-close-button-hide>
</script>Removes the ✕ from the panel header. Mostly for inline embeds, where there’s no bubble to collapse back into and a close button would leave an empty hole in your layout. Accepts "true", "1", or the bare attribute. In floating mode the bubble still toggles the panel, so the close button is redundant rather than essential.
Size, shape and colors
All of these come from the appearance config. Defaults shown are what you get if you never touch anything:
| Want to change | Field | Default | Notes |
|---|---|---|---|
| Bubble size | dimensions.fab_size | 64 px | Also scales the icon and the label padding. |
| Bubble shape | fab_label | "" | The bubble is always fully rounded. Empty label → circle (icon only); with a label → pill that grows to fit the text. |
| Panel width | dimensions.panel_width | 380 px | Ignored inline. |
| Panel height | dimensions.panel_height | 620 px | Capped at 100vh - 40px on short screens. Ignored inline. |
| Panel corners | dimensions.border_radius | 24 px | 0 for square corners. Ignored inline (always square). |
| Corner anchor | layout.position | "bottom-right" | Ignored inline. |
| Colors | theme.* | Indigo / cyan on white | Eight tokens — see Appearance. |
| Font | layout.font_family | Inter stack | Empty inherits nothing — the widget is shadow-scoped, so set it explicitly. |
A compact square-cornered bubble in the top-left, for example:
{
"dimensions": { "fab_size": 52, "panel_width": 340, "panel_height": 560, "border_radius": 0 },
"layout": { "position": "top-left" }
}A different look per site or page
The dashboard appearance is per character, so every embed of the same agent looks the same. To vary it — a dark panel on your docs, a light one on marketing, a wider panel on desktop-only pages — point data-appearance-url at your own JSON:
<script
src="https://api.oshara.ai/widget.js"
data-agent="support-bot"
data-appearance-url="https://cdn.yoursite.com/widget/dark.json">
</script>{
"name": "Nova",
"fab_label": "Ask Nova",
"dimensions": { "fab_size": 72, "panel_width": 420, "border_radius": 8 },
"layout": { "position": "bottom-left" },
"theme": { "background_color": "#0F172A", "text_color": "#F1F5F9", "primary_color": "#818CF8" }
}Notes on the override:
- The file must be served with CORS headers your page can read (
Access-Control-Allow-Origin). - Only the keys you include are overridden; everything else falls back to the built-in defaults — not to your dashboard config, which is skipped entirely once you set this URL.
- The JSON may be bare, or wrapped in
data,appearanceorwidget_appearance— all four shapes are accepted. - If the fetch fails the widget logs a warning and boots with default styling rather than breaking.
Tag managers, SPAs and dynamic injection
When a tag manager or bundler makes data-* attributes awkward, set window globals before the script executes:
<script>
window.VOICE_AGENT_SLUG = "support-bot";
window.VOICE_OPEN_CHAT = true;
window.VOICE_INLINE = false;
</script>
<script src="https://api.oshara.ai/widget.js" async></script>Every attribute has a global equivalent — see Configuration. data-* attributes win when both are present.
For single-page apps that mount and unmount the widget on route changes:
// Tear down the current instance — removes the DOM, disconnects audio
window.__OsharaVoiceWidget?.destroy();Re-injecting the script also tears down the previous instance automatically, so a React component that remounts won’t stack up panels. The flip side: only one widget can run per page. Loading a second script tag replaces the first rather than running two agents side by side.
Before it works on your domain
Both the appearance fetch and the call session are gated by the character’s Allowed Origins. In production an unlisted origin — including an empty list — is rejected with 403 Origin not allowed for this agent. Add every domain you embed on under Characters → [your character] → Deploy → Allowed Origins:
https://mysite.com
https://staging.mysite.comTroubleshooting
| Symptom | Cause |
|---|---|
Nothing renders; console shows data-agent (character slug) is required | Missing or misspelled data-agent. |
Nothing renders; network shows 403 | The embedding domain isn’t in Allowed Origins. |
| Widget appears with default indigo styling and your branding is missing | Appearance fetch failed — console logs appearance fetch returned …; using defaults. Check the origin whitelist or your custom appearance URL’s CORS headers. |
data-inline set but nothing visible | Container has no height, isn’t position: relative, or the script tag was hoisted out of it. |
| Panel opens but the call never connects | Microphone permission denied, or the page isn’t served over HTTPS (getUserMedia requires a secure context). |
Next steps
- Configuration — the full
data-*and window-global reference - Appearance & theming — every field in the appearance JSON
- Audio settings — noise cancellation and device pickers
- Forms — agent-driven forms inside the panel