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.
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.
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.
| 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 |
--runbear-chart-1 | 212 100% 38% | 212 95% 68% | First chart series (0.6.0 and later) |
--runbear-chart-2 | 27 96% 45% | 30 95% 55% | Second chart series (0.6.0 and later) |
--runbear-chart-3 | 164 100% 26% | 160 75% 40% | Third chart series (0.6.0 and later) |
--runbear-chart-4 | 322 62% 45% | 322 75% 70% | Fourth chart series (0.6.0 and later) |
Chart colours come only from these variables, never from the agent: a chart's series take
--runbear-chart-1 to --runbear-chart-4 in order.
: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:
- Follow your app's theme switch. Put
class="dark"on<html>,<body>, or any ancestor of the widget. - Switch one widget only. Add the class to the widget element itself:
document.getElementById("runbear-chat-container").classList.add("dark"). - Follow the operating system. Add
data-runbear-theme="auto"to the widget or any ancestor, and the widget followsprefers-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:
:root { --runbear-primary: 221 83% 53%; }
.dark { --runbear-primary: 213 94% 68%; }or, with data-runbear-theme="auto":
: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; the image, link, pager, list, table and chart hooks from 0.6.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-list, .runbear-table, .runbear-chart | A list, table or chart component (0.6.0 and later), on the same per-component element. It stays when the component shows its fallbackText |
.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. From 0.6.0 it sits on the per-component element, the one that also carries .runbear-component and data-runbear-component="card"; on 0.4.x and 0.5.x it was on the card's inner box |
.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 |
.runbear-card-image | The card's image |
.runbear-card-links, .runbear-card-link | The card's links and one link |
.runbear-image, .runbear-image-fallback | An image the widget shows (markdown images included), and the alt text shown when it fails to load |
.runbear-link | A link inside a component |
.runbear-pager, .runbear-pager-content, .runbear-pager-controls | A paged list or table, its current page, and its controls |
.runbear-pager-previous, .runbear-pager-next, .runbear-pager-status | The Previous and Next buttons and the page status |
.runbear-list-title, .runbear-list-items, .runbear-list-item | A list's title, its items, and one item |
.runbear-list-item-image, .runbear-list-item-body | An item's thumbnail and the text beside it |
.runbear-list-item-title, .runbear-list-item-link, .runbear-list-item-subtitle | An item's title, its link, and its subtitle |
.runbear-list-item-meta, .runbear-list-item-meta-entry | An item's facts and one fact |
.runbear-list-item-meta-label, .runbear-list-item-meta-value | A fact's label and value |
.runbear-table-view, .runbear-table-scroll, .runbear-table-grid | A table, its scrolling area, and the grid |
.runbear-table-caption, .runbear-table-head, .runbear-table-body | The table's caption, header row group and body |
.runbear-table-header, .runbear-table-sort, .runbear-table-sort-icon | A column header, its sort button, and the sort icon |
.runbear-table-row, .runbear-table-cell | One row and one cell |
.runbear-table-more, .runbear-table-status | The "…and N more rows" note for rows the table does not show, and the screen-reader sort status |
.runbear-chart-title, .runbear-chart-body | A chart's title and its body |
.runbear-chart-figure, .runbear-chart-svg, .runbear-chart-overlay | The chart figure, its SVG, and the layer over the plot |
.runbear-chart-axis-x, .runbear-chart-axis-y, .runbear-chart-axis-line, .runbear-chart-axis-label | The axes, their lines and their labels |
.runbear-chart-grid, .runbear-chart-baseline | The grid lines and the zero baseline |
.runbear-chart-series, .runbear-chart-series-<n> | Every series, and the series in position <n> from 1 |
.runbear-chart-legend, .runbear-chart-legend-item, .runbear-chart-legend-swatch | The legend, one entry, and the entry's colour swatch |
.runbear-chart-hidden-series | The note under a chart that carried more series than it draws |
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
:rootto the widget element and were renamed from--background,--primaryand the rest to the--runbear-*names above. Rename your overrides:--primarybecomes--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: the CSP requirement
- Response components: what the component hooks style
- Configuration: assistant name and avatar