Embedding Widget
Add an Atlas chat widget to any website with a single script tag — then customize its theme, layout, and behavior.
This page covers content for developers (installation, configuration, theming, and the programmatic API) and end users (using the widget and understanding error states). Most of this page is developer-focused — end users can skip to Using the Widget and Error Handling.
Atlas provides a drop-in chat widget that adds a floating chat bubble to any webpage. Users click the bubble to open a panel and ask questions about your data — no React or build tooling required.
Not sure if the widget is the right fit? See Choosing an Integration to compare the widget, React package, SDK, and REST API.
Prerequisites
- Atlas API server running and accessible over the network
- For React integration:
@useatlas/reactinstalled in your project - For script tag: your Atlas API URL (no build tooling required)
- An API key or auth token for authentication (optional in
noneauth mode)
Quick Start
Add a single <script> tag before </body>:
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
></script>This injects a floating chat bubble in the bottom-right corner. Clicking it opens the Atlas chat panel.
Configuration
Configure the widget via data-* attributes on the script tag:
| Attribute | Required | Default | Description |
|---|---|---|---|
data-api-url | Yes | -- | Base URL of your Atlas API server |
data-api-key | No | -- | API key for authentication (sent as Bearer token) |
data-theme | No | "light" | "light" or "dark" |
data-position | No | "bottom-right" | "bottom-right" or "bottom-left" |
Event Callbacks
Bind callbacks by setting data-on-* attributes to the name of a global function:
| Attribute | Event | Callback Argument |
|---|---|---|
data-on-open | Widget opens | {} |
data-on-close | Widget closes | {} |
data-on-query-complete | Query finishes (reserved — not yet emitted) | { sql?: string, rowCount?: number } |
data-on-error | Widget error | { code?: string, message?: string } |
<script>
// Called when the widget panel opens
function onAtlasOpen() {
console.log("Widget opened");
}
// Called on widget errors — code identifies the error type
function onAtlasError(detail) {
console.error("Widget error:", detail.code, detail.message);
}
</script>
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
data-api-key="sk-..."
data-theme="dark"
data-position="bottom-left"
data-on-open="onAtlasOpen"
data-on-error="onAtlasError"
></script>Programmatic API
After the script loads, window.Atlas exposes the following methods:
| Method | Description |
|---|---|
Atlas.open() | Open the widget panel |
Atlas.close() | Close the widget panel |
Atlas.toggle() | Toggle open/close |
Atlas.ask(question) | Open the widget and send a question |
Atlas.destroy() | Remove widget from DOM, clean up all listeners |
Atlas.on(event, handler) | Bind an event listener ("open", "close", "queryComplete", "error") |
Atlas.setAuthToken(token) | Send an auth token to the widget iframe |
Atlas.setTheme(theme) | Set theme ("light" or "dark") |
Atlas.ask() currently opens the panel but does not submit the query due to a message type mismatch between the loader and the widget iframe (#324). As a workaround, use the iframe postMessage API directly with {type: "atlas:ask", query: "..."}.
Example
// Open the widget and ask a question
Atlas.ask("What was last month's revenue?");
// Listen for errors
Atlas.on("error", (detail) => {
console.error("Widget error:", detail.code, detail.message);
});
// Pass an auth token (e.g. after your app's login flow)
Atlas.setAuthToken("user-jwt-token");
// Clean up when navigating away in a SPA
Atlas.destroy();Pre-load Command Queue
You can queue commands before the widget script finishes loading. Initialize window.Atlas as an array and push commands:
<script>
// Initialize the command queue before widget.js loads
window.Atlas = window.Atlas || [];
// Commands are replayed in order once the widget initializes
Atlas.push(["open"]);
Atlas.push(["ask", "How many users signed up today?"]);
</script>
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
></script>Queued commands are replayed in order once the widget initializes.
TypeScript Support
TypeScript declarations for window.Atlas are available at /widget.d.ts:
curl https://your-atlas-api.example.com/widget.d.ts -o atlas-widget.d.tsAdd it to your project to get type-safe access to the Atlas API:
/// <reference path="./atlas-widget.d.ts" />
// Full type safety for Atlas methods and event payloads
window.Atlas?.ask("What's the churn rate?");The type declarations define these interfaces:
interface AtlasWidgetEventMap {
/** Emitted when the widget panel opens */
open: Record<string, never>;
/** Emitted when the widget panel closes */
close: Record<string, never>;
/** Emitted when a query completes (reserved — not yet emitted) */
queryComplete: { sql?: string; rowCount?: number };
/** Emitted on widget errors */
error: { code?: string; message?: string };
}
interface AtlasWidget {
open(): void;
close(): void;
toggle(): void;
ask(question: string): void;
destroy(): void;
on<K extends keyof AtlasWidgetEventMap>(
event: K,
handler: (detail: AtlasWidgetEventMap[K]) => void,
): void;
setAuthToken(token: string): void;
setTheme(theme: "light" | "dark"): void;
}
interface Window {
/** Atlas widget API — or a command queue array before widget.js loads */
Atlas?: AtlasWidget | Array<[string, ...unknown[]]>;
}CSS Variables
The Atlas widget renders inside an .atlas-root container that defines all design tokens as CSS custom properties. These use the OKLCH color space and follow the shadcn/ui neutral base.
Light Theme Tokens
All variables are scoped to .atlas-root:
| Variable | Default (OKLCH) | Purpose |
|---|---|---|
--radius | 0.625rem | Base border-radius for cards, inputs, buttons |
--background | oklch(1 0 0) | Page / container background |
--foreground | oklch(0.145 0 0) | Primary text color |
--card | oklch(1 0 0) | Card surface background |
--card-foreground | oklch(0.145 0 0) | Card text color |
--popover | oklch(1 0 0) | Popover / dropdown background |
--popover-foreground | oklch(0.145 0 0) | Popover text color |
--primary | oklch(0.205 0 0) | Primary action color (buttons, links) |
--primary-foreground | oklch(0.985 0 0) | Text on primary-colored surfaces |
--secondary | oklch(0.97 0 0) | Secondary surface color |
--secondary-foreground | oklch(0.205 0 0) | Text on secondary surfaces |
--muted | oklch(0.97 0 0) | Muted / disabled backgrounds |
--muted-foreground | oklch(0.556 0 0) | Muted text (placeholders, hints) |
--accent | oklch(0.97 0 0) | Accent highlight background |
--accent-foreground | oklch(0.205 0 0) | Text on accent surfaces |
--destructive | oklch(0.577 0.245 27.325) | Error / destructive action color |
--destructive-foreground | oklch(0.577 0.245 27.325) | Text for destructive elements |
--border | oklch(0.922 0 0) | Border color for inputs, cards, dividers |
--input | oklch(0.922 0 0) | Input field border color |
--ring | oklch(0.708 0 0) | Focus ring color |
--atlas-brand | oklch(0.759 0.148 167.71) | Brand accent (Atlas teal) — drives --primary in the full app |
Dark Theme Tokens
Applied via .dark .atlas-root:
| Variable | Default (OKLCH) | Change from Light |
|---|---|---|
--background | oklch(0.145 0 0) | Near-black surface |
--foreground | oklch(0.985 0 0) | Near-white text |
--card | oklch(0.145 0 0) | Near-black card surface |
--card-foreground | oklch(0.985 0 0) | Near-white card text |
--popover | oklch(0.145 0 0) | Near-black popover surface |
--popover-foreground | oklch(0.985 0 0) | Near-white popover text |
--primary | oklch(0.985 0 0) | Inverted: light on dark |
--primary-foreground | oklch(0.205 0 0) | Dark text on light primary |
--secondary | oklch(0.269 0 0) | Darker gray surface |
--secondary-foreground | oklch(0.985 0 0) | Near-white text on secondary |
--muted | oklch(0.269 0 0) | Darker muted background |
--muted-foreground | oklch(0.708 0 0) | Brighter muted text |
--accent | oklch(0.269 0 0) | Darker accent background |
--accent-foreground | oklch(0.985 0 0) | Near-white accent text |
--destructive | oklch(0.396 0.141 25.723) | Darker, less saturated red |
--destructive-foreground | oklch(0.637 0.237 25.331) | Brighter red for readability |
--border | oklch(0.269 0 0) | Darker borders |
--input | oklch(0.269 0 0) | Darker input borders |
--ring | oklch(0.439 0 0) | Darker focus ring |
Overriding CSS Variables
Override variables by targeting .atlas-root in your page's CSS. Place your overrides after the Atlas stylesheet loads:
<!-- Override Atlas design tokens to match your brand -->
<style>
.atlas-root {
/* Warmer border radius for a softer look */
--radius: 0.75rem;
/* Custom background and text colors */
--background: oklch(0.98 0.005 250);
--foreground: oklch(0.15 0.02 250);
/* Custom border color */
--border: oklch(0.90 0.01 250);
/* Brand color — used for primary actions */
--atlas-brand: oklch(0.65 0.19 250);
}
/* Dark mode overrides — applied when <html> has class="dark" */
.dark .atlas-root {
--background: oklch(0.16 0.01 250);
--foreground: oklch(0.96 0.005 250);
--border: oklch(0.28 0.01 250);
}
</style>/* atlas-overrides.css — load after Atlas styles */
.atlas-root {
/* Match your design system's radius */
--radius: 0.5rem;
/* Use your brand's primary color */
--primary: oklch(0.55 0.2 260);
--primary-foreground: oklch(0.98 0 0);
/* Custom muted tones */
--muted: oklch(0.96 0.01 260);
--muted-foreground: oklch(0.5 0.02 260);
}
.dark .atlas-root {
--primary: oklch(0.75 0.15 260);
--primary-foreground: oklch(0.15 0 0);
--muted: oklch(0.25 0.01 260);
--muted-foreground: oklch(0.65 0.02 260);
}Widget-Specific CSS Variables
When using the script tag or iframe embed, the widget host page defines one additional variable:
| Variable | Set By | Purpose |
|---|---|---|
--atlas-widget-accent | accent query param or atlas:setBranding postMessage | Overrides submit button background, input focus border, and link colors |
This is separate from the .atlas-root tokens — it uses !important overrides on specific elements to apply accent coloring without requiring CSS variable changes.
Theming
Atlas supports "light", "dark", and "system" themes. The "system" mode follows the user's OS preference via prefers-color-scheme.
Theme via Script Tag
<!-- Set a fixed dark theme -->
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
data-theme="dark"
></script>The script tag loader only supports "light" and "dark". For "system" theme support, use the iframe embed directly with ?theme=system.
Theme via Programmatic API
// Switch theme at runtime — e.g. when user toggles your app's theme
Atlas.setTheme("dark");Theme via iframe postMessage
const iframe = document.querySelector("iframe");
// Send a theme change to the iframe — only "light" and "dark" are valid
iframe.contentWindow.postMessage(
{ type: "theme", value: "dark" },
"https://your-atlas-api.example.com",
);Brand Colors
Atlas supports two brand color mechanisms depending on the embedding approach:
For the React component (@useatlas/react):
The --atlas-brand CSS variable accepts an OKLCH color value. In the full Atlas app, --atlas-brand drives the --primary token, affecting buttons, links, and focus rings. The Atlas API's /api/health endpoint can return a brandColor field which the component applies automatically via applyBrandColor().
/* Override the brand color in your stylesheet */
.atlas-root {
--atlas-brand: oklch(0.62 0.2 275); /* Purple brand */
}For the widget embed (script tag / iframe):
The accent parameter takes a 3- or 6-digit hex color (without #). This sets --atlas-widget-accent and overrides the submit button, input focus border, and link colors.
<!-- Indigo accent via iframe query parameter -->
<iframe
src="https://your-atlas-api.example.com/widget?accent=4f46e5&theme=dark"
title="Atlas Chat"
allow="clipboard-write"
style="width: 400px; height: 600px; border: none; border-radius: 12px;"
></iframe>Complete Brand Theming Example
This example combines a custom logo, accent color, welcome message, and dark theme to match a fictional "Acme Analytics" brand:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Acme Analytics</title>
<style>
body {
margin: 0;
background: #0f172a;
font-family: system-ui, sans-serif;
}
/* Container for the embedded Atlas widget */
.analytics-panel {
max-width: 480px;
height: 700px;
margin: 2rem auto;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.analytics-panel iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div class="analytics-panel">
<!-- Embed with full brand customization:
- theme=dark: dark background
- accent=6366f1: indigo buttons and focus rings
- logo: custom Acme logo (must be HTTPS)
- welcome: greeting shown before first message -->
<iframe
src="https://api.acme.com/widget?theme=dark&accent=6366f1&logo=https%3A%2F%2Facme.com%2Flogo-white.png&welcome=Hi%21%20Ask%20me%20anything%20about%20your%20Acme%20data."
title="Acme Analytics Chat"
allow="clipboard-write"
></iframe>
</div>
<script>
// Update branding at runtime (e.g. after loading user preferences)
const iframe = document.querySelector("iframe");
window.addEventListener("message", (event) => {
// Wait for the widget to signal it's ready
if (event.data?.type === "atlas:ready") {
// Now safe to send branding updates
iframe.contentWindow.postMessage({
type: "atlas:setBranding",
accent: "6366f1", // Indigo accent
welcome: "Welcome back!", // Updated greeting
}, "https://api.acme.com");
}
});
</script>
</body>
</html>postMessage API Reference
The widget communicates with its parent page via the postMessage API. There are two communication channels:
- Host page → Widget iframe — control the widget from your application
- Widget iframe → Host page — receive events from the widget
Always specify the target origin instead of "*" in production. Using "*" allows any page to send messages to your widget, which could be exploited if the page is embedded elsewhere.
Host → Widget Messages
These messages are sent from your page to the widget iframe via iframe.contentWindow.postMessage(message, origin).
theme — Set the color theme
// Set the widget to dark mode
iframe.contentWindow.postMessage(
{
type: "theme", // Message discriminator
value: "dark", // "light" or "dark" — "system" is not supported via postMessage
},
"https://your-atlas-api.example.com", // Target origin — must match your API URL
);auth — Pass an authentication token
// Send a JWT or API key to the widget — it becomes the Bearer token for API requests
iframe.contentWindow.postMessage(
{
type: "auth", // Message discriminator
token: "eyJhbG...", // Your auth token string — sent as Authorization: Bearer <token>
},
"https://your-atlas-api.example.com",
);toggle — Show or hide the widget
// Toggle the widget's visibility (shown/hidden)
iframe.contentWindow.postMessage(
{
type: "toggle", // No additional fields needed
},
"https://your-atlas-api.example.com",
);atlas:ask — Send a query programmatically
// Programmatically type and submit a question
iframe.contentWindow.postMessage(
{
type: "atlas:ask", // Must include the "atlas:" prefix
query: "Show revenue by region", // The question to send
},
"https://your-atlas-api.example.com",
);atlas:setBranding — Update branding at runtime
// Update logo, accent color, or welcome message without reloading
iframe.contentWindow.postMessage(
{
type: "atlas:setBranding",
logo: "https://example.com/logo.png", // Optional — HTTPS URLs only
accent: "dc2626", // Optional — hex without #, 3 or 6 digits
welcome: "Welcome! Ask me about your data.", // Optional — max 500 characters
},
"https://your-atlas-api.example.com",
);Each field in atlas:setBranding is optional — only include the fields you want to update. Logo URLs must use HTTPS or they will be silently ignored.
Widget → Host Messages
The widget sends these messages to the parent window via window.parent.postMessage(). Listen for them with window.addEventListener("message", handler).
atlas:ready — Widget loaded successfully
window.addEventListener("message", (event) => {
// Always check the origin to prevent spoofed messages
if (event.origin !== "https://your-atlas-api.example.com") return;
if (event.data?.type === "atlas:ready") {
console.log("Atlas widget is ready");
// Safe to send postMessage commands now (auth, branding, queries)
}
});atlas:error — Error occurred
window.addEventListener("message", (event) => {
if (event.origin !== "https://your-atlas-api.example.com") return;
if (event.data?.type === "atlas:error") {
// Error codes: "UNCAUGHT", "UNHANDLED_REJECTION", "RENDER_FAILED", "LOAD_FAILED"
console.error(
`Atlas error [${event.data.code}]:`,
event.data.message,
);
}
});TypeScript Types for postMessage
Use these types in your application for type-safe message handling:
// ---- Host → Widget messages ----
/** Set the widget theme */
interface AtlasThemeMessage {
type: "theme";
value: "light" | "dark"; // "system" not supported via postMessage
}
/** Pass an auth token to the widget */
interface AtlasAuthMessage {
type: "auth";
token: string; // Sent as Authorization: Bearer <token>
}
/** Toggle widget visibility */
interface AtlasToggleMessage {
type: "toggle";
}
/** Send a query to the widget */
interface AtlasAskMessage {
type: "atlas:ask";
query: string;
}
/** Update branding at runtime — all fields optional */
interface AtlasSetBrandingMessage {
type: "atlas:setBranding";
logo?: string; // HTTPS URL only
accent?: string; // Hex color without # (3 or 6 digits)
welcome?: string; // Max 500 characters
}
type HostToWidgetMessage =
| AtlasThemeMessage
| AtlasAuthMessage
| AtlasToggleMessage
| AtlasAskMessage
| AtlasSetBrandingMessage;
// ---- Widget → Host messages ----
/** Widget loaded successfully */
interface AtlasReadyMessage {
type: "atlas:ready";
}
/** Widget error — includes error code and human-readable message */
interface AtlasErrorMessage {
type: "atlas:error";
code: "UNCAUGHT" | "UNHANDLED_REJECTION" | "RENDER_FAILED" | "LOAD_FAILED";
message: string;
}
type WidgetToHostMessage = AtlasReadyMessage | AtlasErrorMessage;Complete postMessage Example
<iframe
id="atlas-widget"
src="https://your-atlas-api.example.com/widget?theme=light"
title="Atlas Chat"
allow="clipboard-write"
style="width: 400px; height: 600px; border: none; border-radius: 12px;"
></iframe>
<script>
const iframe = document.getElementById("atlas-widget");
const ATLAS_ORIGIN = "https://your-atlas-api.example.com";
// Listen for messages from the widget
window.addEventListener("message", (event) => {
// Security: always check origin
if (event.origin !== ATLAS_ORIGIN) return;
switch (event.data?.type) {
case "atlas:ready":
// Widget is initialized — send auth token and initial config
iframe.contentWindow.postMessage(
{ type: "auth", token: getAuthToken() },
ATLAS_ORIGIN,
);
iframe.contentWindow.postMessage(
{ type: "atlas:setBranding", accent: "4f46e5" },
ATLAS_ORIGIN,
);
break;
case "atlas:error":
// Log errors to your monitoring service
reportError({
source: "atlas-widget",
code: event.data.code,
message: event.data.message,
});
break;
}
});
// Sync theme when your app's theme changes
function onAppThemeChange(newTheme) {
iframe.contentWindow.postMessage(
{ type: "theme", value: newTheme }, // "light" or "dark"
ATLAS_ORIGIN,
);
}
</script>Layout Options
The widget supports three layout modes: floating bubble (default for the script tag), inline embed, and full-page.
Floating Bubble (Default)
The script tag loader creates a floating bubble in the bottom corner. Clicking it opens a 400×600 panel.
<!-- Floating bubble in the bottom-right corner (default) -->
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
></script><!-- Floating bubble in the bottom-left corner -->
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
data-position="bottom-left"
></script>The bubble has these fixed properties:
- Size: 56×56px circle
- Z-index: 2147483646 (just below maximum)
- Panel size: 400×600px, capped at
calc(100vh - 108px)height andcalc(100vw - 40px)width - Animation: Scale + opacity entrance, cubic-bezier open/close transition
- Keyboard: Escape key closes the panel
Inline Embed
Embed the widget directly as an iframe with explicit dimensions. This gives you full control over placement — put it in a sidebar, a modal, or anywhere in your layout.
<!-- Inline embed — sized to fit a sidebar or card -->
<iframe
src="https://your-atlas-api.example.com/widget?theme=light"
title="Atlas Chat"
allow="clipboard-write"
style="width: 400px; height: 600px; border: none; border-radius: 12px;"
></iframe>With branding:
<!-- Inline embed with full branding customization -->
<iframe
src="https://your-atlas-api.example.com/widget?theme=dark&accent=4f46e5&welcome=Ask%20me%20anything&logo=https%3A%2F%2Fexample.com%2Flogo.png"
title="Atlas Chat"
allow="clipboard-write"
style="width: 100%; height: 600px; border: none; border-radius: 12px;"
></iframe>Widget Query Parameters
These parameters are set on the /widget iframe URL:
| Parameter | Default | Description |
|---|---|---|
theme | "system" | "light", "dark", or "system" — the iframe supports all three |
apiUrl | iframe origin | Atlas API base URL (must be http:// or https://) |
position | "inline" | "bottomRight", "bottomLeft", or "inline" — set by the script loader |
logo | -- | HTTPS URL to a custom logo image (replaces the default Atlas logo) |
accent | -- | Hex color without # (e.g. 4f46e5) — overrides submit button, focus ring, and link colors |
welcome | -- | Welcome message shown before the first user message (max 500 chars) |
initialQuery | -- | Auto-sends this query when the widget first opens (max 500 chars) |
Full-Page Mode
Make the iframe fill the entire viewport for a dedicated analytics page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Analytics — Powered by Atlas</title>
<style>
/* Remove all margins and make iframe fill the viewport */
* { margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
iframe { width: 100%; height: 100%; border: none; }
</style>
</head>
<body>
<!-- Full-viewport Atlas chat — no border-radius for edge-to-edge display -->
<iframe
src="https://your-atlas-api.example.com/widget?theme=system&welcome=What%20would%20you%20like%20to%20know%3F"
title="Atlas Analytics"
allow="clipboard-write"
></iframe>
</body>
</html>Common Failures
CORS Errors
Symptom: Browser console shows Access to fetch at 'https://your-api...' has been blocked by CORS policy.
Cause: The Atlas API sets Access-Control-Allow-Origin: * on widget routes, but your reverse proxy or CDN may strip or override these headers.
Fix: Ensure your reverse proxy forwards CORS headers from the Atlas API. If you use nginx:
# nginx — forward CORS headers from the Atlas API for widget routes
location /widget {
proxy_pass http://atlas-api:3001;
# Don't strip Access-Control headers set by Atlas
proxy_pass_request_headers on;
}If you override CORS at the proxy level, include these headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationDo not combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers reject this per the CORS spec. Widget routes use wildcard origin and do not require credentialed requests.
iframe Sandbox Restrictions
Symptom: Widget loads but shows a blank page, or JavaScript errors appear in the iframe's console.
Cause: If you add a sandbox attribute to the iframe, it blocks scripts, forms, and same-origin access by default.
Fix: If you must use sandbox, include the required permissions:
<!-- Minimum sandbox permissions for Atlas to function -->
<iframe
src="https://your-atlas-api.example.com/widget"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
title="Atlas Chat"
allow="clipboard-write"
></iframe>allow-scripts and allow-same-origin together effectively disable sandboxing. If security isolation is your goal, host the widget on a separate subdomain instead.
Content Security Policy (CSP)
Symptom: Widget script or iframe blocked. Console shows Refused to load the script or Refused to frame.
Cause: Your site's CSP headers don't allow loading resources from the Atlas API domain.
Fix: Add the Atlas API domain to your CSP:
Content-Security-Policy:
script-src 'self' https://your-atlas-api.example.com;
frame-src 'self' https://your-atlas-api.example.com;
connect-src 'self' https://your-atlas-api.example.com;If you use a <meta> tag for CSP:
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://your-atlas-api.example.com;
frame-src 'self' https://your-atlas-api.example.com;
connect-src 'self' https://your-atlas-api.example.com;">Auth Token Not Reaching the Widget
Symptom: Widget loads but shows "Unauthorized" or API key prompt even though you passed a token.
Causes and fixes:
Check: Token sent before widget is ready
The widget iframe must finish loading before it can receive postMessage commands. Always wait for the atlas:ready event:
// WRONG — widget may not be ready yet
iframe.contentWindow.postMessage({ type: "auth", token: myToken }, origin);
// CORRECT — wait for the widget to signal readiness
window.addEventListener("message", (event) => {
if (event.origin !== ATLAS_ORIGIN) return;
if (event.data?.type === "atlas:ready") {
// Widget is ready — now send the token
iframe.contentWindow.postMessage(
{ type: "auth", token: myToken },
ATLAS_ORIGIN,
);
}
});Check: Origin mismatch
The widget only accepts messages from window.parent. If your iframe is nested inside another iframe, the parent origin check will fail. Ensure your page is the direct parent of the Atlas iframe.
Also verify the origin you pass to postMessage matches the actual widget URL:
// The origin must match the iframe's src domain exactly
const ATLAS_ORIGIN = "https://your-atlas-api.example.com"; // No trailing slash
iframe.contentWindow.postMessage(
{ type: "auth", token: myToken },
ATLAS_ORIGIN,
);Check: Using HTTPS
Auth tokens are transmitted via postMessage. If either the host page or the widget is served over HTTP, tokens can be intercepted. Use HTTPS for both in production.
Check: Script tag with data-api-key
If using the script tag loader, verify the data-api-key attribute is set on the correct <script> element and the key is valid:
<!-- The API key goes on the widget.js script tag, not a separate script -->
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
data-api-key="sk-your-actual-key"
></script>Widget Shows "Unable to Load Atlas Chat"
Symptom: Widget displays a gray error message instead of the chat interface.
Causes:
- The widget JS bundle is not built — run
bun run buildinpackages/react/ - The
/widget/atlas-widget.jsor/widget/atlas-widget.cssassets return 404 - A JavaScript error occurred during initialization (check the iframe's console)
Diagnosis: Open browser DevTools, switch to the iframe's context, and check the console for errors. The widget logs all errors with the [Atlas Widget] prefix and sends them to the parent via atlas:error postMessage.
Error Handling
The widget handles errors automatically with contextual UI. This section documents what happens for each error type and how to listen for errors from your host page. This is primarily for developers integrating the widget.
Error States
| Condition | User-facing message | Icon | Recovery |
|---|---|---|---|
| API unreachable | "Unable to connect to Atlas." | ServerCrash | Retry button (manual) |
| Auth failure | Auth-mode-specific (e.g. "Your session has expired.") | ShieldAlert | Re-authenticate |
| Browser offline | "You appear to be offline." | WifiOff | Auto-retries when navigator.onLine restores |
| Rate limited | "Too many requests." + countdown | Clock | Auto-retries after countdown reaches 0 |
| Server error (5xx) | "Something went wrong on our end." | ServerCrash | Retry button (manual) |
| Unknown error | "Something went wrong. Please try again." | AlertTriangle | Retry button (manual) |
Errors are classified client-side before the server response is parsed. The ClientErrorCode type covers five cases:
type ClientErrorCode =
| "api_unreachable" // fetch failed, ECONNREFUSED, ENOTFOUND
| "auth_failure" // HTTP 401
| "rate_limited_http" // HTTP 429
| "server_error" // HTTP 5xx
| "offline"; // navigator.onLine === falseServer-side errors are parsed from the JSON response body and mapped to a ChatErrorCode. See the React Hooks reference for the full list.
Listening for Errors
Script Tag (Programmatic API)
// Listen for errors via the Atlas.on() method
Atlas.on("error", (detail) => {
// detail: { code?: string, message?: string }
console.error("Widget error:", detail.code, detail.message);
// Report to your monitoring service
myErrorTracker.capture("atlas-widget-error", detail);
});Script Tag (Data Attribute Callback)
<script>
function onAtlasError(detail) {
console.error("Widget error:", detail.code, detail.message);
}
</script>
<script
src="https://your-atlas-api.example.com/widget.js"
data-api-url="https://your-atlas-api.example.com"
data-on-error="onAtlasError"
></script>iframe postMessage
The widget emits atlas:error messages to the parent window for every error. The payload includes the error code, title, detail, and retryability:
window.addEventListener("message", (event) => {
// Always check origin to prevent spoofed messages
if (event.origin !== "https://your-atlas-api.example.com") return;
if (event.data?.type === "atlas:error") {
const { code, message, detail, retryable } = event.data.error;
// code: ClientErrorCode or ChatErrorCode (e.g. "api_unreachable", "auth_error")
// message: user-facing title (e.g. "Unable to connect to Atlas.")
// detail: optional secondary context
// retryable: true for transient errors, false for permanent ones
if (!retryable) {
// Permanent error — show your own fallback UI or redirect to login
showFallbackUI(message);
}
}
});React Component
When using the AtlasChat React component, errors are rendered inline automatically. For programmatic access, use the useAtlasChat hook:
import { useAtlasChat, parseChatError } from "@useatlas/react/hooks";
function ChatUI() {
const { error, status } = useAtlasChat();
if (error) {
// parseChatError extracts structured info from the AI SDK error
const info = parseChatError(error, "simple-key");
console.log(info.title); // "Unable to connect to Atlas."
console.log(info.clientCode); // "api_unreachable"
console.log(info.retryable); // true
}
// ... render chat UI
}Auth Token Refresh
When a token expires mid-session, the widget shows an auth error. To refresh the token without reloading the page:
// When your app refreshes a token, push it to the widget
function onTokenRefresh(newToken) {
Atlas.setAuthToken(newToken);
}
// Example: refresh before expiry using a JWT decode
const payload = JSON.parse(atob(token.split(".")[1]));
const expiresIn = payload.exp * 1000 - Date.now();
setTimeout(async () => {
const newToken = await refreshAuthToken();
Atlas.setAuthToken(newToken);
}, expiresIn - 60_000); // Refresh 1 minute before expiryconst iframe = document.querySelector("iframe");
const ATLAS_ORIGIN = "https://your-atlas-api.example.com";
// Send a fresh token when the old one expires
function onTokenRefresh(newToken) {
iframe.contentWindow.postMessage(
{ type: "auth", token: newToken },
ATLAS_ORIGIN,
);
}
// Proactive refresh: send a new token before expiry
setInterval(async () => {
const newToken = await myApp.refreshToken();
onTokenRefresh(newToken);
}, 15 * 60 * 1000); // Every 15 minutesAPI Unreachable Behavior
When the Atlas API is unreachable (server down, network error, DNS failure), the widget:
- Shows an inline error banner with the
ServerCrashicon and "Unable to connect to Atlas." - Displays the detail "Check your API URL configuration and ensure the server is running."
- Shows a "Try again" button for manual retry
- Emits an
atlas:errorpostMessage to the parent window withcode: "api_unreachable"andretryable: true
The widget does not auto-retry on API unreachable — the user must click "Try again" or you must programmatically retry by re-sending the query.
Custom Tool Renderers
Override how tool results render inside the widget using the toolRenderers prop on <AtlasChat> (React component) or via the headless hooks. This is useful for matching your product's design system or adding custom interactions like CSV export buttons.
How It Works
Every tool invocation in the agent's response is rendered by a component. Custom renderers take precedence over built-in defaults. If no custom renderer is provided, the widget falls back to its built-in cards (SQL result table, explore output, Python charts).
import { AtlasChat } from "@useatlas/react";
import "@useatlas/react/styles.css";
function App() {
return (
<AtlasChat
apiUrl="https://your-atlas-api.example.com"
apiKey="sk-..."
toolRenderers={{
executeSQL: MySQLRenderer, // Override SQL result display
executePython: MyPythonRenderer, // Override Python output display
// explore uses the built-in default (not overridden)
}}
/>
);
}Renderer Props
Every custom renderer receives ToolRendererProps<T>:
interface ToolRendererProps<T = unknown> {
toolName: string; // Name of the tool (e.g. "executeSQL")
args: Record<string, unknown>; // Input arguments passed to the tool
result: T; // Tool output — null while the tool is running
isLoading: boolean; // Whether the tool invocation is still in progress
}Tool Result Field Reference
executeSQL — SQL query results
Type: SQLToolResult | null (null while loading)
Success shape:
| Field | Type | Description |
|---|---|---|
success | true | Discriminant — always true on success |
columns | string[] | Column names in query order |
rows | Record<string, unknown>[] | Array of row objects keyed by column name |
truncated | boolean | undefined | Whether results were truncated by the row limit |
explanation | string | undefined | Agent's natural-language explanation of the query |
row_count | number | undefined | Total row count before truncation |
Error shape:
| Field | Type | Description |
|---|---|---|
success | false | Discriminant — always false on error |
error | string | Error message from the database or validator |
Example custom renderer:
import type { ToolRendererProps, SQLToolResult } from "@useatlas/react";
function MySQLRenderer({ result, isLoading, args }: ToolRendererProps<SQLToolResult | null>) {
if (isLoading || !result) {
return <div className="animate-pulse p-4">Running query...</div>;
}
if (!result.success) {
return <div className="text-red-500 p-4">Query failed: {result.error}</div>;
}
return (
<div className="border rounded-lg overflow-hidden">
{/* Show the SQL that was executed */}
{args.sql && (
<pre className="bg-gray-100 p-2 text-xs overflow-x-auto">
{String(args.sql)}
</pre>
)}
{/* Result count */}
<div className="px-3 py-1 text-sm text-gray-500">
{result.rows.length} rows{result.truncated ? " (truncated)" : ""}
</div>
{/* Data table */}
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-gray-50">
{result.columns.map((col) => (
<th key={col} className="px-3 py-2 text-left font-medium">{col}</th>
))}
</tr>
</thead>
<tbody>
{result.rows.map((row, i) => (
<tr key={i} className="border-b">
{result.columns.map((col) => (
<td key={col} className="px-3 py-2">{String(row[col] ?? "")}</td>
))}
</tr>
))}
</tbody>
</table>
{/* Agent explanation */}
{result.explanation && (
<p className="px-3 py-2 text-sm text-gray-600 italic">{result.explanation}</p>
)}
</div>
);
}explore — Semantic layer exploration
Type: ExploreToolResult | null (null while loading)
The explore tool returns a plain string — the output of the semantic layer file read or search command.
| Field | Type | Description |
|---|---|---|
| (entire result) | string | Raw text output from the explore command |
Example custom renderer:
import type { ToolRendererProps, ExploreToolResult } from "@useatlas/react";
function MyExploreRenderer({ result, isLoading, args }: ToolRendererProps<ExploreToolResult | null>) {
if (isLoading || result === null) {
return <div className="animate-pulse p-4">Exploring schema...</div>;
}
return (
<details className="border rounded-lg">
<summary className="px-3 py-2 cursor-pointer text-sm font-medium">
{/* args.command contains the explore command that was run */}
Explore: {String(args.command ?? "semantic layer")}
</summary>
<pre className="p-3 text-xs overflow-x-auto whitespace-pre-wrap bg-gray-50">
{result}
</pre>
</details>
);
}executePython — Python code execution
Type: PythonToolResult | null (null while loading)
Success shape:
| Field | Type | Description |
|---|---|---|
success | true | Discriminant — always true on success |
output | string | undefined | stdout/stderr text output |
explanation | string | undefined | Agent's natural-language explanation |
table | { columns: string[]; rows: unknown[][] } | undefined | Tabular output (column-ordered, not keyed) |
charts | { base64: string; mimeType: "image/png" }[] | undefined | Static chart images (matplotlib, etc.) |
rechartsCharts | Array<{ type: "line" | "bar" | "pie"; data: Record<string, unknown>[]; categoryKey: string; valueKeys: string[] }> | undefined | Interactive chart data for Recharts rendering |
Error shape:
| Field | Type | Description |
|---|---|---|
success | false | Discriminant — always false on error |
error | string | Error message from the Python runtime |
output | string | undefined | Any stdout captured before the error |
Example custom renderer:
import type { ToolRendererProps, PythonToolResult } from "@useatlas/react";
function MyPythonRenderer({ result, isLoading }: ToolRendererProps<PythonToolResult | null>) {
if (isLoading || !result) {
return <div className="animate-pulse p-4">Running Python...</div>;
}
if (!result.success) {
return (
<div className="border border-red-200 rounded-lg p-4">
<p className="text-red-600 font-medium">Execution failed</p>
<pre className="text-xs mt-2">{result.error}</pre>
{result.output && <pre className="text-xs mt-2 text-gray-500">{result.output}</pre>}
</div>
);
}
return (
<div className="space-y-3">
{/* Text output */}
{result.output && (
<pre className="bg-gray-50 p-3 rounded text-xs overflow-x-auto">{result.output}</pre>
)}
{/* Static chart images */}
{result.charts?.map((chart, i) => (
<img
key={i}
src={`data:${chart.mimeType};base64,${chart.base64}`}
alt={`Chart ${i + 1}`}
className="max-w-full rounded"
/>
))}
{/* Interactive Recharts data — render with your preferred chart library */}
{result.rechartsCharts?.map((chart, i) => (
<div key={i} className="border rounded p-3">
<p className="text-sm font-medium mb-2">
{chart.type} chart — {chart.categoryKey} vs {chart.valueKeys.join(", ")}
</p>
{/* Plug in your own chart component here */}
<pre className="text-xs">{JSON.stringify(chart.data.slice(0, 3), null, 2)}</pre>
</div>
))}
{/* Tabular output */}
{result.table && (
<table className="w-full text-sm border">
<thead>
<tr>
{result.table.columns.map((col) => (
<th key={col} className="px-2 py-1 border-b bg-gray-50 text-left">{col}</th>
))}
</tr>
</thead>
<tbody>
{result.table.rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} className="px-2 py-1 border-b">{String(cell ?? "")}</td>
))}
</tr>
))}
</tbody>
</table>
)}
</div>
);
}Custom / Plugin Tools
Any tool name can have a custom renderer. Plugin tools (e.g. from BigQuery, Salesforce, or custom plugins) are passed through with ToolRendererProps<unknown>:
import type { ToolRendererProps } from "@useatlas/react";
// Renderer for a custom "searchDocs" plugin tool
function DocsSearchRenderer({ result, isLoading, args }: ToolRendererProps) {
if (isLoading || !result) return <div>Searching docs...</div>;
// Cast to your expected shape
const data = result as { results: { title: string; url: string }[] };
return (
<ul>
{data.results.map((doc, i) => (
<li key={i}>
<a href={doc.url} target="_blank" rel="noopener noreferrer">{doc.title}</a>
</li>
))}
</ul>
);
}
// Register in toolRenderers
<AtlasChat
apiUrl="..."
toolRenderers={{
searchDocs: DocsSearchRenderer,
}}
/>Using Custom Renderers with Headless Hooks
The headless hooks expose tool invocations via the parts array on each message. Use ToolRendererProps types to build your own rendering logic:
import { useAtlasChat } from "@useatlas/react/hooks";
import type { SQLToolResult } from "@useatlas/react/hooks";
function ChatUI() {
const { messages } = useAtlasChat();
return (
<div>
{messages.map((msg) =>
msg.parts?.map((part, i) => {
if (part.type === "text") return <p key={i}>{part.text}</p>;
if (part.type === "tool-invocation") {
// Access tool name, args, and result from the part
const { toolName, args, result, state } = part.toolInvocation;
const isLoading = state !== "result";
if (toolName === "executeSQL" && result) {
const sqlResult = result as SQLToolResult;
// Render your custom SQL table here
}
}
return null;
}),
)}
</div>
);
}CSS Customization Guide
The Atlas widget uses CSS custom properties (variables) scoped to .atlas-root for all visual design tokens. These follow the shadcn/ui neutral base with the OKLCH color space.
Quick Reference
All variables are listed in the CSS Variables section above. Here are the most common customization recipes.
Brand Color Override
Change the primary brand color across the widget — affects buttons, links, and focus rings:
<!-- Use the accent query parameter (hex without #) -->
<iframe
src="https://your-atlas-api.example.com/widget?accent=6366f1"
title="Atlas Chat"
allow="clipboard-write"
style="width: 400px; height: 600px; border: none;"
></iframe>
<!-- Or update at runtime via postMessage -->
<script>
iframe.contentWindow.postMessage(
{ type: "atlas:setBranding", accent: "6366f1" },
"https://your-atlas-api.example.com",
);
</script>import { useEffect } from "react";
import { AtlasChat } from "@useatlas/react";
import "@useatlas/react/styles.css";
// Override via CSS — the brand color drives --primary in the full app
// Place this in your app's CSS, after the Atlas styles import
// .atlas-root { --atlas-brand: oklch(0.55 0.2 275); }
// Or override at runtime via the useAtlasTheme hook
import { useAtlasTheme } from "@useatlas/react/hooks";
function BrandSetup() {
const { applyBrandColor } = useAtlasTheme();
// Call once on mount — sets --atlas-brand on :root
useEffect(() => applyBrandColor("oklch(0.55 0.2 275)"), []);
return null;
}/* Brand color — indigo */
.atlas-root {
--atlas-brand: oklch(0.55 0.2 275);
--primary: oklch(0.55 0.2 275);
--primary-foreground: oklch(0.98 0 0);
}
.dark .atlas-root {
--primary: oklch(0.7 0.15 275);
--primary-foreground: oklch(0.15 0 0);
}Dark Mode Override
Force dark mode regardless of user preference, or customize the dark theme colors:
/* Force dark mode on the Atlas widget */
.atlas-root {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
}Or use the data-theme attribute / Atlas.setTheme() / ?theme=dark query parameter to let the widget handle dark mode with its built-in tokens.
Font Override
The widget inherits font-family from its container. Override it by targeting .atlas-root or specific elements:
/* Set a custom font for the entire widget */
.atlas-root {
font-family: "Inter", system-ui, sans-serif;
}
/* Or target just the input field */
[data-atlas-input] {
font-family: "JetBrains Mono", monospace;
font-size: 14px;
}
/* Override message text font */
[data-atlas-messages] {
font-family: "Merriweather", serif;
line-height: 1.7;
}Complete Brand Theming Example
Combine all customizations for a fully branded widget:
/* Acme Analytics — branded Atlas widget */
.atlas-root {
/* Brand indigo */
--atlas-brand: oklch(0.55 0.2 275);
--primary: oklch(0.55 0.2 275);
--primary-foreground: oklch(0.98 0 0);
/* Warmer background */
--background: oklch(0.99 0.005 275);
--foreground: oklch(0.12 0.02 275);
/* Rounded corners */
--radius: 0.75rem;
/* Subtle brand-tinted borders */
--border: oklch(0.92 0.01 275);
--input: oklch(0.92 0.01 275);
/* Custom font */
font-family: "Inter", system-ui, sans-serif;
}
.dark .atlas-root {
--primary: oklch(0.7 0.15 275);
--primary-foreground: oklch(0.15 0 0);
--background: oklch(0.13 0.02 275);
--foreground: oklch(0.96 0.005 275);
--border: oklch(0.25 0.01 275);
--input: oklch(0.25 0.01 275);
}Data Attribute Selectors
Key widget elements expose data-* attributes for targeted styling:
| Selector | Element |
|---|---|
[data-atlas-input] | The chat text input field |
[data-atlas-form] | The input form container |
[data-atlas-messages] | The scrollable messages container |
[data-atlas-logo] | The Atlas logo SVG |
.atlas-root | The root container (all design tokens live here) |
Security Considerations
Allowed Origins: The widget script and iframe routes set Access-Control-Allow-Origin: * and Content-Security-Policy: frame-ancestors * to allow embedding from any domain. If you need to restrict origins, configure your reverse proxy or CDN to override these headers.
Auth Tokens: When using data-api-key or Atlas.setAuthToken(), the token is sent to the widget iframe via postMessage. Use HTTPS to prevent token interception. Prefer short-lived tokens over long-lived API keys for production deployments.
Logo URLs: Custom logos must use HTTPS. Non-HTTPS URLs are silently rejected to prevent mixed content and javascript: / data: URI attacks.
Accent Colors: The accent parameter is validated as a 3- or 6-digit hex string. Invalid values are silently ignored.
Troubleshooting
Widget loads but shows "Unable to connect to Atlas"
Cause: The data-api-url attribute (script tag) or apiUrl query parameter (iframe) doesn't match your running Atlas API server, or CORS headers are being stripped by a reverse proxy.
Fix: Verify the URL is correct and reachable from the browser. Check the browser console for CORS errors. See CORS Errors above for proxy configuration.
Widget appears but chat input is unresponsive
Cause: The widget JavaScript bundle failed to load or a Content Security Policy is blocking scripts from the Atlas API domain.
Fix: Open browser DevTools, check the Console and Network tabs for blocked requests. Add the Atlas API domain to your CSP script-src and connect-src directives. See Content Security Policy (CSP) above.
Widget doesn't match your site's theme
Cause: The widget defaults to light theme (script tag) or system theme (iframe). It doesn't automatically inherit your site's CSS.
Fix: Set data-theme="dark" on the script tag, or use ?theme=dark on the iframe URL. For dynamic theming, use Atlas.setTheme() or the theme postMessage. See Theming for details.
For more, see Troubleshooting.
See Also
- React Hooks Reference — Headless React hooks for building custom chat UIs
- SDK Reference — TypeScript SDK for server-side and headless integrations
- Choosing an Integration — Compare widget, hooks, SDK, and REST API
- Rate Limiting & Retry — How the widget handles 429 responses
- Authentication — Auth mode setup for widget deployments
Using the Widget
This section is for end users interacting with the Atlas widget embedded in a website or application.
Asking questions
Click the chat bubble (or the embedded chat panel) to start a conversation. Type your question in natural language — for example, "What was last month's revenue?" or "Show me the top 10 customers by order count." The agent translates your question into a SQL query, runs it against your database, and returns the results with a narrative explanation.
Understanding responses
Responses include:
- Answer text — A plain-language summary of the results
- SQL query — The exact query that was run (shown in a code block)
- Data table — A formatted table of results (limited to the first rows for readability)
- Charts — Visual representations when the agent generates them
Rate limits
If you send too many requests in a short period, you will see a "Too many requests" message with a countdown timer. Wait for the countdown to complete, then try again. This limit protects the database from excessive load.
Error messages
If something goes wrong, the widget shows a contextual error message with a recovery action (e.g., "Try again" button, or "Your session has expired" with a re-authentication prompt). You do not need to do anything technical — the widget handles errors and guides you toward resolution.