# Web SDK theming

> Restyle the Runbear chat widget with --runbear-* CSS variables, turn on dark mode, and target its stable class and data hooks from your own CSS.

Source: https://docs.runbear.io/api/web-sdk/theming

Last updated: 2026-09-30

The widget ships its own stylesheet and keeps it to itself: every rule it emits is scoped to
`[data-runbear]`, the attribute on the widget's own element. It declares nothing on your `:root`
and doesn't restyle your application. You restyle the widget with CSS variables, and target its
parts through stable class and data hooks.

This page describes 0.3.0 and later. If you are upgrading from 0.2.x, see
[Migrating from 0.2.x](#migrating-from-02x).

> **Note**
>
> The stylesheet is injected as an inline `<style>` element, so a Content-Security-Policy must allow
> `style-src 'unsafe-inline'`. See [Install](/api/web-sdk/install.md#content-security-policy).

## CSS variables

Set any of these on **any ancestor of the widget**, including `:root`, and the widget picks them
up. The names below are the public contract; variables not listed here are internal and may change
in any release.

> **Values are HSL triplets**
>
> Write each color as `H S% L%`, space separated, with **no** `hsl()` wrapper and **no** hex. The
> widget composes colors as `hsl(var(--runbear-primary) / 0.9)` for hover and disabled states, which
> only parses when the variable holds bare channel values. `--runbear-primary: #1d4ed8` fails
> silently: the colors that use it stop rendering, with no error.

| Variable                           | Light default    | Dark default     | Used for                             |
| ---------------------------------- | ---------------- | ---------------- | ------------------------------------ |
| `--runbear-background`             | `0 0% 100%`      | `240 10% 3.9%`   | Widget surface                       |
| `--runbear-foreground`             | `240 10% 3.9%`   | `0 0% 98%`       | Body text                            |
| `--runbear-border`                 | `240 5.9% 90%`   | `240 3.7% 15.9%` | Dividers and outlines                |
| `--runbear-primary`                | `240 5.9% 10%`   | `0 0% 98%`       | Send button, user message, tooltips  |
| `--runbear-primary-foreground`     | `0 0% 98%`       | `240 5.9% 10%`   | Text on `primary`                    |
| `--runbear-secondary`              | `240 4.8% 95.9%` | `240 3.7% 15.9%` | Secondary buttons                    |
| `--runbear-secondary-foreground`   | `240 5.9% 10%`   | `0 0% 98%`       | Text on `secondary`                  |
| `--runbear-muted`                  | `240 4.8% 95.9%` | `240 3.7% 15.9%` | Muted surfaces, hover states         |
| `--runbear-muted-foreground`       | `240 3.8% 46.1%` | `240 5% 64.9%`   | Placeholder and helper text          |
| `--runbear-accent`                 | `240 4.8% 95.9%` | `240 3.7% 15.9%` | Hovered controls                     |
| `--runbear-accent-foreground`      | `240 5.9% 10%`   | `0 0% 98%`       | Text on `accent`                     |
| `--runbear-destructive`            | `0 84.2% 60.2%`  | `0 62.8% 30.6%`  | Destructive actions                  |
| `--runbear-destructive-foreground` | `0 0% 98%`       | `0 0% 98%`       | Text on `destructive`                |
| `--runbear-input`                  | `240 5.9% 90%`   | `240 3.7% 15.9%` | Input borders                        |
| `--runbear-ring`                   | `240 10% 3.9%`   | `240 4.9% 83.9%` | Focus rings                          |
| `--runbear-radius`                 | `0.5rem`         | `0.5rem`         | Corner radius. A length, not a color |

```css
:root {
  --runbear-primary: 221 83% 53%;
  --runbear-primary-foreground: 0 0% 100%;
  --runbear-radius: 0.75rem;
}
```

## Dark mode

Dark mode is off unless you ask for it. There are three ways, in the order most sites want them:

1. **Follow your app's theme switch.** Put `class="dark"` on `<html>`, `<body>`, or any ancestor of
   the widget.
2. **Switch one widget only.** Add the class to the widget element itself:
   `document.getElementById("runbear-chat-container").classList.add("dark")`.
3. **Follow the operating system.** Add `data-runbear-theme="auto"` to the widget or any ancestor,
   and the widget follows `prefers-color-scheme`. This is opt-in, so a site with its own light and
   dark control never has the widget disagree with it.

A `--runbear-*` value you set applies to **both** palettes. To differ per theme, scope the override
the same way you turned dark mode on:

```css
:root { --runbear-primary: 221 83% 53%; }
.dark { --runbear-primary: 213 94% 68%; }
```

or, with `data-runbear-theme="auto"`:

```css
:root { --runbear-primary: 221 83% 53%; }

@media (prefers-color-scheme: dark) {
  [data-runbear-theme="auto"] { --runbear-primary: 213 94% 68%; }
}
```

## Class and data hooks

These names are stable and safe to target from your own CSS.

**The widget**

| Hook                                                                                          | Element                                               |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `[data-runbear]`, `#runbear-chat-container`, `.runbear-chat-window`                           | The widget's outer element                            |
| `.runbear-chat-surface`                                                                       | The layout element inside it, where the UI is mounted |
| `.runbear-suggestions`                                                                        | The suggested-questions block                         |
| `#runbear-chat-form`                                                                          | The message form                                      |
| `#runbear-chat-input-textarea`                                                                | The message box                                       |
| `#runbear-chat-input-button`, `#runbear-chat-stop-button`, `#runbear-chat-attachments-button` | The send, stop and attachment buttons                 |

**Response components** (0.3.0 and later; the card hooks from 0.4.0)

| Hook                                                               | Element                                                                                                                                    |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `[data-runbear-components]`                                        | The wrapper around all of one message's components                                                                                         |
| `.runbear-component`, `[data-runbear-component="<name>"]`          | One component, whatever its name. The attribute carries the component's name, so a component added later is targetable without a new class |
| `[data-runbear-state="live"]`, `[data-runbear-state="inert"]`      | On the same element: whether the component still accepts a click                                                                           |
| `.runbear-confirm`, `.runbear-select`                              | A `confirm` or `select` component                                                                                                          |
| `.runbear-component-options`, `[data-runbear-component-options]`   | The group of option buttons                                                                                                                |
| `.runbear-component-option`, `[data-runbear-component-option]`     | One option button                                                                                                                          |
| `[data-runbear-component-disclosure]`                              | The "Label → value" lines under the buttons                                                                                                |
| `.runbear-component-fallback`, `[data-runbear-component-fallback]` | The `fallbackText` shown when a component can't be rendered                                                                                |
| `.runbear-card`                                                    | A card                                                                                                                                     |
| `.runbear-card-title`, `.runbear-card-subtitle`                    | The card's title and subtitle                                                                                                              |
| `.runbear-card-fields`, `.runbear-card-field`                      | The card's field list and one field                                                                                                        |
| `.runbear-card-field-label`, `.runbear-card-field-value`           | A field's label and value                                                                                                                  |

An inert option button carries `aria-disabled="true"`, not the `disabled` attribute, so it stays
focusable and readable. Style it with `[aria-disabled="true"]`, not `:disabled`.

## Overriding a rule

Utility classes the SDK generates apply to the **descendants** of the widget's outer element.
Nothing the SDK emits uses `!important`, and its rules have the specificity of one attribute plus
one class.

The SDK's stylesheet is appended to `<head>` when the module is imported, so it is the last sheet in
the document and wins every tie. To override a rule, be **more** specific:
`#runbear-chat-container .runbear-suggestions button { … }` overrides; a bare
`.runbear-suggestions button { … }` doesn't. The `--runbear-*` variables need none of this: the
widget reads them from your side of the cascade, so they always apply.

The only rules that reach outside the widget are two that set Tailwind's private `--tw-*` custom
properties on every element. They affect nothing but Tailwind's own internals.

## Migrating from 0.2.x

0.3.0 isolated the widget's styles, which is a breaking change for any site that themed it:

- The theme variables moved from `:root` to the widget element and were renamed from
  `--background`, `--primary` and the rest to the `--runbear-*` names above. Rename your overrides:
  `--primary` becomes `--runbear-primary`, and so on.
- `--popover*`, `--card*` and `--chart-*` were removed.
- The widget no longer restyles the rest of your page, so anything on your page that relied on the
  SDK's variables needs its own.

## Related

- [Install](/api/web-sdk/install.md#content-security-policy): the CSP requirement
- [Response components](/api/web-sdk/response-components.md): what the component hooks style
- [Configuration](/api/web-sdk/configuration.md): assistant name and avatar
