Skip to content

Theming

The chat widget uses Shadow DOM for style isolation. Override appearance via CSS custom properties — they pierce the shadow boundary cleanly.

CSS variables

VariableDefault (light)Default (dark)Purpose
--bridle-primary#0070f3#0070f3Brand color (FAB, send button, user bubbles)
--bridle-primary-fg#ffffff#ffffffForeground on primary
--bridle-bg#ffffff#0f172aPanel background
--bridle-fg#111827#f1f5f9Body text
--bridle-muted#6b7280#94a3b8Secondary text
--bridle-bubble-bg#f3f4f6#1e293bAssistant bubble background
--bridle-border#e5e7eb#334155Borders, dividers
--bridle-radius14px14pxPanel + bubble corner radius
--bridle-shadow0 12px 32px rgba(0,0,0,.16)0 12px 32px rgba(0,0,0,.16)Panel/FAB shadow
--bridle-z21474836002147483600z-index of the floating panel
--bridle-fontsystem stacksystem stackFont family

The widget switches to its dark palette automatically when prefers-color-scheme: dark. Explicit overrides win.

Override via CSS

The simplest approach — target the custom element directly:

css
bridle-chat {
  --bridle-primary: #ec4899;       /* hot pink */
  --bridle-primary-fg: #ffffff;
  --bridle-radius: 8px;
  --bridle-font: 'Inter', sans-serif;
}

Place this in your site's regular stylesheet. The variables propagate through the shadow boundary.

Override via the theme option

When using init() programmatically, pass a theme map. Keys can include or omit the leading --:

ts
init({
  apiUrl,
  agentId,
  token,
  theme: {
    '--bridle-primary': '#0070f3',
    'bridle-radius': '8px',          // also accepted
    'bridle-bubble-bg': '#f0f9ff',
  },
})

These set style="..." on the <bridle-chat> element, so they win over external CSS without requiring !important.

Override via data-* (script tag)

Drop-in embeds can ship overrides via two attributes:

html
<script
  src="https://bridle.cleanslice.org/sdk/latest.js"
  data-agent-id="agent-abc-123"
  data-stylesheet="/css/bridle-overrides.css"
></script>
AttributeDescription
data-custom-cssInline CSS string injected into the shadow root
data-stylesheetOne URL, or several comma-separated URLs, loaded as <link rel="stylesheet"> inside the shadow root

See Overriding internal classes below for what these unlock.

Overriding internal classes

CSS variables cover the design tokens (colors, radius, shadow, font). To restyle the actual class rules — .bridle__panel, .bridle__bubble, .bridle__header, etc. — you need to inject CSS inside the shadow root. Host-page CSS can't reach those selectors.

init() accepts two options:

ts
init({
  apiUrl,
  agentId,
  token,
  // Inline CSS — appended as <style> inside the shadow root.
  customCss: `
    .bridle__panel {
      border-radius: 5px;
      box-shadow: 0 2px 6px #00000029;
      border: 1px solid #C5D5FF;
    }
    .bridle__bubble {
      font-size: 13px;
    }
  `,
  // External file(s) — appended as <link rel="stylesheet"> inside the shadow root.
  stylesheets: ['/css/bridle-overrides.css'],
})

Drop-in equivalent:

html
<script
  src="https://bridle.cleanslice.org/sdk/latest.js"
  data-agent-id="agent-abc-123"
  data-stylesheet="/css/bridle-overrides.css, /css/bridle-typography.css"
></script>

Cascade order: your overrides are appended after the component's own <style>, so equal-specificity rules win without !important.

Internal classes you can target

ClassWhat it is
.bridle__panelThe chat panel (floating window or inline container)
.bridle__headerHeader row with title and close button
.bridle__messagesScrollable message list
.bridle__bubbleA single message bubble (assistant or user)
.bridle__bubble--mdAssistant bubble with rendered Markdown
.bridle__msg--user / .bridle__msg--assistantBubble wrapper per role
.bridle__inputFooter composer (textarea + send button)
.bridle__fabFloating action button (floating mode only)
.bridle__fab-iconCustom <img> inside the FAB when fabIcon is set
.bridle__typingThree-dot typing indicator
.bridle__banner--errorConnection-error banner
.bridle__attachPaperclip button that opens the image picker
.bridle__attachmentsThumbnail strip above the input for staged image attachments
.bridle__attachmentOne staged attachment thumbnail (56×56)
.bridle__attachment-imgThe thumbnail image inside an attachment
.bridle__attachment-removeThe × button on a thumbnail
.bridle__drop-overlayTinted dashed overlay shown while a file is being dragged over the panel
.bridle__drop-hint"Drop image to attach" label inside the drop overlay
.bridle__msg-imageImage rendered inside a message bubble (user or assistant)

These class names are part of the SDK's public surface — they won't be renamed without a major-version bump.

Match a specific brand

A few common combos as a starting point:

css
/* GitHub-ish */
bridle-chat {
  --bridle-primary: #2da44e;
  --bridle-radius: 6px;
}

/* Vercel-ish */
bridle-chat {
  --bridle-primary: #000000;
  --bridle-primary-fg: #ffffff;
  --bridle-radius: 8px;
}

/* Stripe-ish */
bridle-chat {
  --bridle-primary: #635bff;
  --bridle-radius: 10px;
}

Positioning the floating bubble

The widget defaults to bottom-right at right: 20px; bottom: 20px. To move it, target the host element:

css
bridle-chat {
  /* widget itself is fixed-positioned in floating mode — set inset on the host */
  right: auto;
  left: 20px;       /* bottom-left */
  bottom: 100px;    /* avoid covering an existing UI element */
}

Custom FAB icon

The floating button in the bottom-right corner ships with a built-in chat-bubble glyph. To swap it for your own, pass fabIcon — any URL the browser can render in <img> works (.svg, .png, .webp, or a data: URI). The container stays 56×56 with the brand background (--bridle-primary); only the glyph changes.

ts
init({
  apiUrl,
  agentId,
  token,
  fabIcon: '/icons/chat.svg',
})

Drop-in equivalent via data-fab-icon:

html
<script
  src="https://bridle.cleanslice.org/sdk/latest.js"
  data-agent-id="agent-abc-123"
  data-token="<jwt>"
  data-fab-icon="/icons/chat.svg"
></script>

Inline SVG via data: URI

Skips the extra HTTP request and lets you bake the color into the SVG itself (currentColor is not honored from an <img> src). Encode # as %23:

ts
init({
  apiUrl,
  agentId,
  token,
  fabIcon:
    "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' " +
    "viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' " +
    "stroke-linecap='round' stroke-linejoin='round'>" +
    "<path d='M12 2L2 7l10 5 10-5-10-5z'/>" +
    "<path d='M2 17l10 5 10-5'/>" +
    "<path d='M2 12l10 5 10-5'/>" +
    "</svg>",
})

Resizing the button

The FAB is 56×56 by default and the icon inside is 24×24. To enlarge both, target the internal classes via customCss:

ts
init({
  apiUrl, agentId, token,
  fabIcon: '/icons/chat.svg',
  customCss: `
    .bridle__fab { width: 64px; height: 64px; }
    .bridle__fab-icon { width: 28px; height: 28px; }
  `,
})

Sizing in inline mode

In mode: 'inline', the widget fills its mount container. Set the dimensions on the parent:

html
<div id="chat" style="width: 380px; height: 600px;"></div>
<script src=".../sdk/latest.js"
        data-mode="inline"
        data-mount="#chat"
        data-agent-id="..."
        data-token="..."></script>

Custom font

The widget inherits the font from --bridle-font. To use Inter or a custom font:

html
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">

<style>
  bridle-chat { --bridle-font: 'Inter', sans-serif; }
</style>

The font itself must be loaded by the host page — Shadow DOM doesn't fetch web fonts on its own.

Released under the MIT License.