Atlas
Guides

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/react installed in your project
  • For script tag: your Atlas API URL (no build tooling required)
  • An API key or auth token for authentication (optional in none auth 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:

AttributeRequiredDefaultDescription
data-api-urlYes--Base URL of your Atlas API server
data-api-keyNo--API key for authentication (sent as Bearer token)
data-themeNo"light""light" or "dark"
data-positionNo"bottom-right""bottom-right" or "bottom-left"

Event Callbacks

Bind callbacks by setting data-on-* attributes to the name of a global function:

AttributeEventCallback Argument
data-on-openWidget opens{}
data-on-closeWidget closes{}
data-on-query-completeQuery finishes (reserved — not yet emitted){ sql?: string, rowCount?: number }
data-on-errorWidget 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:

MethodDescription
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.ts

Add 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:

VariableDefault (OKLCH)Purpose
--radius0.625remBase border-radius for cards, inputs, buttons
--backgroundoklch(1 0 0)Page / container background
--foregroundoklch(0.145 0 0)Primary text color
--cardoklch(1 0 0)Card surface background
--card-foregroundoklch(0.145 0 0)Card text color
--popoveroklch(1 0 0)Popover / dropdown background
--popover-foregroundoklch(0.145 0 0)Popover text color
--primaryoklch(0.205 0 0)Primary action color (buttons, links)
--primary-foregroundoklch(0.985 0 0)Text on primary-colored surfaces
--secondaryoklch(0.97 0 0)Secondary surface color
--secondary-foregroundoklch(0.205 0 0)Text on secondary surfaces
--mutedoklch(0.97 0 0)Muted / disabled backgrounds
--muted-foregroundoklch(0.556 0 0)Muted text (placeholders, hints)
--accentoklch(0.97 0 0)Accent highlight background
--accent-foregroundoklch(0.205 0 0)Text on accent surfaces
--destructiveoklch(0.577 0.245 27.325)Error / destructive action color
--destructive-foregroundoklch(0.577 0.245 27.325)Text for destructive elements
--borderoklch(0.922 0 0)Border color for inputs, cards, dividers
--inputoklch(0.922 0 0)Input field border color
--ringoklch(0.708 0 0)Focus ring color
--atlas-brandoklch(0.759 0.148 167.71)Brand accent (Atlas teal) — drives --primary in the full app

Dark Theme Tokens

Applied via .dark .atlas-root:

VariableDefault (OKLCH)Change from Light
--backgroundoklch(0.145 0 0)Near-black surface
--foregroundoklch(0.985 0 0)Near-white text
--cardoklch(0.145 0 0)Near-black card surface
--card-foregroundoklch(0.985 0 0)Near-white card text
--popoveroklch(0.145 0 0)Near-black popover surface
--popover-foregroundoklch(0.985 0 0)Near-white popover text
--primaryoklch(0.985 0 0)Inverted: light on dark
--primary-foregroundoklch(0.205 0 0)Dark text on light primary
--secondaryoklch(0.269 0 0)Darker gray surface
--secondary-foregroundoklch(0.985 0 0)Near-white text on secondary
--mutedoklch(0.269 0 0)Darker muted background
--muted-foregroundoklch(0.708 0 0)Brighter muted text
--accentoklch(0.269 0 0)Darker accent background
--accent-foregroundoklch(0.985 0 0)Near-white accent text
--destructiveoklch(0.396 0.141 25.723)Darker, less saturated red
--destructive-foregroundoklch(0.637 0.237 25.331)Brighter red for readability
--borderoklch(0.269 0 0)Darker borders
--inputoklch(0.269 0 0)Darker input borders
--ringoklch(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:

VariableSet ByPurpose
--atlas-widget-accentaccent query param or atlas:setBranding postMessageOverrides 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:

  1. Host page → Widget iframe — control the widget from your application
  2. 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 and calc(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:

ParameterDefaultDescription
theme"system""light", "dark", or "system" — the iframe supports all three
apiUrliframe originAtlas 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, Authorization

Do 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 build in packages/react/
  • The /widget/atlas-widget.js or /widget/atlas-widget.css assets 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

ConditionUser-facing messageIconRecovery
API unreachable"Unable to connect to Atlas."ServerCrashRetry button (manual)
Auth failureAuth-mode-specific (e.g. "Your session has expired.")ShieldAlertRe-authenticate
Browser offline"You appear to be offline."WifiOffAuto-retries when navigator.onLine restores
Rate limited"Too many requests." + countdownClockAuto-retries after countdown reaches 0
Server error (5xx)"Something went wrong on our end."ServerCrashRetry button (manual)
Unknown error"Something went wrong. Please try again."AlertTriangleRetry 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 === false

Server-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 expiry
const 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 minutes

API Unreachable Behavior

When the Atlas API is unreachable (server down, network error, DNS failure), the widget:

  1. Shows an inline error banner with the ServerCrash icon and "Unable to connect to Atlas."
  2. Displays the detail "Check your API URL configuration and ensure the server is running."
  3. Shows a "Try again" button for manual retry
  4. Emits an atlas:error postMessage to the parent window with code: "api_unreachable" and retryable: 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:

FieldTypeDescription
successtrueDiscriminant — always true on success
columnsstring[]Column names in query order
rowsRecord<string, unknown>[]Array of row objects keyed by column name
truncatedboolean | undefinedWhether results were truncated by the row limit
explanationstring | undefinedAgent's natural-language explanation of the query
row_countnumber | undefinedTotal row count before truncation

Error shape:

FieldTypeDescription
successfalseDiscriminant — always false on error
errorstringError 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.

FieldTypeDescription
(entire result)stringRaw 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:

FieldTypeDescription
successtrueDiscriminant — always true on success
outputstring | undefinedstdout/stderr text output
explanationstring | undefinedAgent's natural-language explanation
table{ columns: string[]; rows: unknown[][] } | undefinedTabular output (column-ordered, not keyed)
charts{ base64: string; mimeType: "image/png" }[] | undefinedStatic chart images (matplotlib, etc.)
rechartsChartsArray<{ type: "line" | "bar" | "pie"; data: Record<string, unknown>[]; categoryKey: string; valueKeys: string[] }> | undefinedInteractive chart data for Recharts rendering

Error shape:

FieldTypeDescription
successfalseDiscriminant — always false on error
errorstringError message from the Python runtime
outputstring | undefinedAny 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:

SelectorElement
[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-rootThe 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


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.

On this page

Quick StartConfigurationEvent CallbacksProgrammatic APIExamplePre-load Command QueueTypeScript SupportCSS VariablesLight Theme TokensDark Theme TokensOverriding CSS VariablesWidget-Specific CSS VariablesThemingTheme via Script TagTheme via Programmatic APITheme via iframe postMessageBrand ColorsComplete Brand Theming ExamplepostMessage API ReferenceHost → Widget Messagestheme — Set the color themeauth — Pass an authentication tokentoggle — Show or hide the widgetatlas:ask — Send a query programmaticallyatlas:setBranding — Update branding at runtimeWidget → Host Messagesatlas:ready — Widget loaded successfullyatlas:error — Error occurredTypeScript Types for postMessageComplete postMessage ExampleLayout OptionsFloating Bubble (Default)Inline EmbedWidget Query ParametersFull-Page ModeCommon FailuresCORS Errorsiframe Sandbox RestrictionsContent Security Policy (CSP)Auth Token Not Reaching the WidgetCheck: Token sent before widget is readyCheck: Origin mismatchCheck: Using HTTPSCheck: Script tag with data-api-keyWidget Shows "Unable to Load Atlas Chat"Error HandlingError StatesListening for ErrorsScript Tag (Programmatic API)Script Tag (Data Attribute Callback)iframe postMessageReact ComponentAuth Token RefreshAPI Unreachable BehaviorCustom Tool RenderersHow It WorksRenderer PropsTool Result Field ReferenceexecuteSQL — SQL query resultsexplore — Semantic layer explorationexecutePython — Python code executionCustom / Plugin ToolsUsing Custom Renderers with Headless HooksCSS Customization GuideQuick ReferenceBrand Color OverrideDark Mode OverrideFont OverrideComplete Brand Theming ExampleData Attribute SelectorsSecurity ConsiderationsTroubleshootingWidget loads but shows "Unable to connect to Atlas"Widget appears but chat input is unresponsiveWidget doesn't match your site's themeSee AlsoUsing the WidgetAsking questionsUnderstanding responsesRate limitsError messages