Krispy AI
guides

embed + theme the widget

drop the one-line script on any page, then theme it from KV without a redeploy.

embed + theme the widget

widget.js is dependency-free and lives in a Shadow DOM, so your site's CSS can't leak in (and vice versa). host it anywhere static โ€” your CDN, the edge Worker's origin, an R2 / Pages bucket.

embed

drop one tag on any page, before </body>:

<script
  src="https://YOUR-HOST/widget.js"
  data-api="https://krispy-edge.YOU.workers.dev"
  data-tenant="self"
  async
></script>

script attributes

these are the boot-time knobs read off the <script> tag. richer appearance is set via the theme (below), not attributes.

attributerequireddefaultmeaning
data-apiyesโ€”your @krispy/edge Worker base URL
data-tenantnoselftenant id (multi-tenant uses this)
data-titlenoChat with usheader text (theme headerTitle supersedes it)
data-accentno#e39a2bbrand color, used before the theme fetch lands

theme (from KV, no redeploy)

on open, the widget fetches GET /api/widget/config?t=<tenant> โ€” a public, secret-free projection of your tenant's theme. change it in KV (via the CLI or your own tooling writing POST /api/tenant/config) and the widget picks it up on next load; no rebuild.

the themeable fields (WidgetTheme):

fielddefaultwhat it controls
primaryColor#e39a2b (gold)header, visitor bubble, send button
launcherColor= primaryColorthe floating launcher button only
positionbrbr (bottom-right) or bl (bottom-left)
avatarโ€”"buttr" (inline default croissant) or an https:// URL
greetingโ€”first bot bubble shown when the panel opens
headerTitleโ€”header text (supersedes data-title)
radius14panel/bubble corner radius, px, clamped 0โ€“20
fontsystem stacka CSS font-family stack
soundtruenotification "ding" on inbound messages while the panel is closed

theme values cross a CSS trust boundary. the widget sanitizes them: colors must be #hex or rgb(...), radius is clamped 0โ€“20, fonts reject ;{}<>, and avatars must be the literal buttr or an https:// URL. anything else is dropped. see security.

what the widget does

  • markdown โ€” bot/operator bubbles render a safe subset (**bold**, *italic*, `code`, [label](url)) built with createElement/textContent, never innerHTML. visitor and system messages stay literal. links only render for http(s)/mailto.
  • notifications โ€” a WebAudio "ding" + launcher pulse + unread dot, fired only on an inbound message while the panel is closed. respects a per-visitor mute (persisted) and the browser autoplay policy (silent until first interaction).
  • mobile โ€” a keyboard-aware floating card via visualViewport, 16px inputs to dodge iOS zoom, and safe-area insets.
  • resilience โ€” the operator WebSocket reconnects with exponential backoff + jitter; the chat still works over plain POST if the socket is down.

local demo

cd services/edge && bunx wrangler dev    # Worker on :8787
bunx serve packages/widget               # demo page (index.html points data-api at :8787)

serve over http so the WebSocket and localStorage work.

On this page