Skip to content
GitHub

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.


On this page

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.

VariableLight defaultDark defaultUsed for
--runbear-background0 0% 100%240 10% 3.9%Widget surface
--runbear-foreground240 10% 3.9%0 0% 98%Body text
--runbear-border240 5.9% 90%240 3.7% 15.9%Dividers and outlines
--runbear-primary240 5.9% 10%0 0% 98%Send button, user message, tooltips
--runbear-primary-foreground0 0% 98%240 5.9% 10%Text on primary
--runbear-secondary240 4.8% 95.9%240 3.7% 15.9%Secondary buttons
--runbear-secondary-foreground240 5.9% 10%0 0% 98%Text on secondary
--runbear-muted240 4.8% 95.9%240 3.7% 15.9%Muted surfaces, hover states
--runbear-muted-foreground240 3.8% 46.1%240 5% 64.9%Placeholder and helper text
--runbear-accent240 4.8% 95.9%240 3.7% 15.9%Hovered controls
--runbear-accent-foreground240 5.9% 10%0 0% 98%Text on accent
--runbear-destructive0 84.2% 60.2%0 62.8% 30.6%Destructive actions
--runbear-destructive-foreground0 0% 98%0 0% 98%Text on destructive
--runbear-input240 5.9% 90%240 3.7% 15.9%Input borders
--runbear-ring240 10% 3.9%240 4.9% 83.9%Focus rings
--runbear-radius0.5rem0.5remCorner radius. A length, not a color
: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:

: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

HookElement
[data-runbear], #runbear-chat-container, .runbear-chat-windowThe widget's outer element
.runbear-chat-surfaceThe layout element inside it, where the UI is mounted
.runbear-suggestionsThe suggested-questions block
#runbear-chat-formThe message form
#runbear-chat-input-textareaThe message box
#runbear-chat-input-button, #runbear-chat-stop-button, #runbear-chat-attachments-buttonThe send, stop and attachment buttons

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

HookElement
[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-selectA 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-cardA card
.runbear-card-title, .runbear-card-subtitleThe card's title and subtitle
.runbear-card-fields, .runbear-card-fieldThe card's field list and one field
.runbear-card-field-label, .runbear-card-field-valueA 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.