Widget runtime
Treat the browser embed as a small, stable runtime surface.
The widget loader exposes a queue-based global API, a single singleton iframe instance, token refresh support, and an event bridge for host analytics and UI coordination.
Use this guide when you already know the widget is part of the workflow
This page is for engineers and platform teams who need exact runtime behavior after Quickstart, not for teams that are still deciding whether they need the widget at all.
Best for host-app control
Use this when your app owns the trigger, layout, page placement, or analytics hooks around the widget.
Best for richer embed patterns
Use this when you need inline changelog blocks, module-scoped views, or host-controlled panel behavior.
Not the first stop
If your only goal is to get something live, Quickstart is still the better path. Come here when implementation details start to matter.
Minimal runtime call
Start with tokenProvider and tenant defaults. Only override presentation fields when the host app truly owns that decision.
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => {
const response = await fetch("/api/widget-token", {
credentials: "include",
});
const { token, expiresInSeconds } = await response.json();
return { token, expiresInSeconds };
},
});Most common widget workflows
Most teams use the same runtime in a few repeatable ways.
Launcher or panel for global updates
Best when updates should be available from anywhere in the product with one consistent entry point.
Inline changelog for a specific page
Best when onboarding, settings, billing, or admin pages should show release notes in-place instead of in a floating shell.
Host-owned triggers and analytics
Best when your app already has command surfaces, navigation, or event tracking you want the widget to plug into cleanly.
Configuration reference
These are the public config fields that matter for most implementations.
| Field | Required | Behavior |
|---|---|---|
| tenantId | Recommended | Required when using tenant runtime defaults. Identifies which tenant runtime config to fetch. |
| token | Optional | Short-lived widget JWT. Use only when you are not using tokenProvider. |
| tokenProvider | Optional | Async function that returns a token string or { token, expiresInSeconds | expiresAt }. |
| useTenantDefaults | Optional | When true, loads the defaults saved in the dashboard: mode, position, theme, trigger, label, header copy, logo, brand colour, design options, custom CSS, sizing, max items, locale, and z-index. Any field you also pass to init wins over the saved default. |
| mode | Optional, default "launcher" | Supported values are "launcher", "panel", and "inline". |
| showTrigger | Optional, default true | Hide the built-in trigger when your host app owns the button. |
| position | Optional, default "bottom-right" | Floating position for launcher and panel modes: "bottom-right", "bottom-left", "top-right", or "top-left". |
| theme | Optional, default "light" | "light", "dark", or "auto". "auto" follows the visitor's system colour scheme and switches live when it changes. |
| brandColor | Optional, default "#0f172a" | Accent colour for the launcher, header rule, and card accents. See Appearance options for the accepted formats. |
| radius / density / font / surface / shadow / cardStyle | Optional | Design options. See Appearance options below for values and defaults. |
| customCss | Optional | A stylesheet injected inside the widget, in both the closed launcher and the open panel. See Custom CSS below. |
| launcherLabel | Optional, default "Product updates" | Text on the built-in launcher or panel trigger. Also used as the header title when no title is set. |
| title / subtitle | Optional | Override the in-widget header copy. Pass subtitle: "" to show no subtitle without hiding the header. |
| eyebrow | Optional | Small uppercase label shown above the header title. Shows nothing unless you set one. |
| logoUrl | Optional | An https image URL shown next to the header title. Non-https URLs are ignored, and an image that fails to load falls back to the default mark. |
| hideHeader | Inline only | When true, removes the header entirely. Ignored in launcher and panel modes. |
| locale | Optional, default "en" | Locale used to format entry dates: "en", "es", or "fr". Unsupported values fall back to "en". |
| maxItems | Optional, default 8 | How many entries to load per page. Capped at 50. |
| zIndex | Optional, default 2147483000 | Stacking order of the widget iframe on your page. Lower it if the widget covers your own modals. |
| width / panelWidth / triggerWidth | Optional | Dimension overrides for floating layouts. Strings or numbers are supported. |
| target | Inline only | Selector or mount target for inline mode. Required if there is no [data-chainlog-widget] container. |
| autoHeight | Inline only | Defaults to true. Resize messages from the frame adjust height automatically. |
| contextModule | Optional | Pass a product-area trigger such as billing or dashboard to filter visible entries. |
| changelogUrl | Optional | Full changelog URL shown as a link inside the panel. Resolved automatically when useTenantDefaults is true. |
| open | Optional | Force open/closed state. When omitted, panel and launcher modes default closed and persist the visitor's choice in localStorage, auto-opening once per genuinely new entry. |
| onEvent | Optional | Receives widget lifecycle, click, filter, height, and error events. |
Appearance options
Set these in the dashboard under Widget → Appearance, or pass them to ChainlogWidget.init. Values passed to init override the dashboard defaults.
| Option | Values | What it changes |
|---|---|---|
| theme | "light" (default), "dark", "auto" | "auto" follows the visitor's system colour scheme. |
| brandColor | "#0f172a" (default) | A hex colour (#rgb, #rgba, #rrggbb, #rrggbbaa), a colour function (rgb, rgba, hsl, hsla, hwb, lab, lch, oklab, oklch, color) with no nested parentheses, or a named colour such as teal. Anything else falls back to the default. |
| radius | "round" (default), "soft", "sharp" | Corner rounding of the frame, cards, sections, controls, and pills. |
| density | "comfortable" (default), "compact" | Padding in the header, body, and cards, and the gap between entries. |
| font | "default" (default), "system", "serif", "mono" | "default" uses the widget's own typeface. For your own font, see Custom fonts below. |
| surface | "gradient" (default), "flat" | "flat" replaces the background gradients and highlights with solid colours and gives the launcher a solid brand fill. |
| shadow | "soft" (default), "none", "strong" | Depth of the launcher, section, and card shadows. |
| cardStyle | "accent" (default), "bordered", "minimal" | "accent" draws a brand-tinted bar on each card, "bordered" uses a stronger outline, "minimal" removes the border and shadow. |
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: getWidgetToken,
// These override whatever is saved in the dashboard.
theme: "auto",
brandColor: "#e11d48",
radius: "soft",
density: "compact",
font: "system",
surface: "flat",
shadow: "none",
cardStyle: "bordered",
});Unknown values fall back to the default
Each design option accepts only the values listed. Anything else, including a typo, uses the default for that option, and a brandColor that is not a plain colour uses #0f172a.
Presets
A preset is a starting set of design values. Applying one fills in the options above; you can change any of them afterwards, and the widget stores the individual values rather than the preset name.
| Preset | radius, density, font, surface, shadow, cardStyle | Look |
|---|---|---|
| Classic | round, comfortable, default, gradient, soft, accent | Soft gradients, rounded cards, and a brand accent bar. This is the default look. |
| Minimal | soft, comfortable, system, flat, none, minimal | Flat, borderless, and quiet. Lets your content lead. |
| Bold | round, comfortable, default, flat, strong, bordered | Solid brand launcher, strong shadows, and outlined cards. |
| Compact | sharp, compact, system, flat, soft, bordered | Tighter spacing and smaller corners for dense layouts. |
Custom CSS
Add a stylesheet in the dashboard under Widget → Appearance → Custom CSS (up to 10,000 characters), or pass customCss to init. It is injected inside the widget, in both the closed launcher and the open panel.
| Class | Element |
|---|---|
| .chainlog-frame | The widget container in panel and inline modes, and the open launcher panel. |
| .chainlog-launcher | The closed launcher button. |
| .chainlog-header | The header with the title, eyebrow, subtitle, and logo. |
| .chainlog-browse | The browse and filter bar above the entry list. |
| .chainlog-entry-card | Each entry card in the list. |
| .chainlog-entry-title | An entry's title. |
| .chainlog-entry-summary | An entry's summary text. |
| .chainlog-entry-tags | The row of tags on an entry card. |
| .chainlog-footer | The footer below the entry list. |
| CSS variable | Controls |
|---|---|
| --chainlog-brand | The brand colour. |
| --chainlog-brand-tint | A light, translucent tint of the brand colour. |
| --chainlog-brand-tint-strong | A stronger translucent tint of the brand colour. |
| --chainlog-launcher-bg | Launcher background. Built from --chainlog-brand. |
| --chainlog-launcher-shadow | Launcher shadow. |
| --chainlog-launcher-sheen | Glossy highlight over the launcher (none on flat surfaces). |
| --chainlog-shell-bg | Background of the widget body. |
| --chainlog-shell-border | Outer border of the widget. |
| --chainlog-shell-glow | Inner top highlight of the widget edge. |
| --chainlog-shell-highlight | Brand glow in the top corner. Built from --chainlog-brand-tint. |
| --chainlog-shell-sheen | Soft sheen across the top of the body. |
| --chainlog-header-bg | Header background. |
| --chainlog-header-rule | Thin line across the top of the header. Built from --chainlog-brand. |
| --chainlog-logo-ring | Ring around the header logo. |
| --chainlog-filter-bg | Background of the browse and filter bar. |
| --chainlog-section-shadow | Shadow on the browse bar and on panels such as loading, empty, and entry detail sections. |
| --chainlog-card-shadow | Shadow on entry cards. |
| --chainlog-card-accent | Accent bar on entry cards. Built from the brand tints; none unless cardStyle is accent. |
| --chainlog-card-border | Entry card border colour. |
| --chainlog-card-border-hover | Entry card border colour on hover. |
| --chainlog-radius-frame | Corner radius of the widget frame. |
| --chainlog-radius-card | Corner radius of entry cards. |
| --chainlog-radius-section | Corner radius of panels such as loading, empty, and entry detail sections. |
| --chainlog-radius-control | Corner radius of the browse bar and filter selects. |
| --chainlog-radius-pill | Corner radius of buttons, chips, and the launcher. |
| --chainlog-header-pad | Header padding on small screens. |
| --chainlog-header-pad-sm | Header padding on wider screens. |
| --chainlog-body-pad | Body padding on small screens. |
| --chainlog-body-pad-sm | Body padding on wider screens. |
| --chainlog-card-pad | Card padding on small screens. |
| --chainlog-card-pad-sm | Card padding on wider screens. |
| --chainlog-list-gap | Space between entries in the list. |
| --chainlog-font | Font family used throughout the widget. |
.chainlog-frame {
--chainlog-brand: #e11d48;
--chainlog-brand-tint: rgba(225, 29, 72, 0.16);
--chainlog-brand-tint-strong: rgba(225, 29, 72, 0.22);
--chainlog-radius-card: 8px;
}
.chainlog-launcher {
--chainlog-launcher-bg: #111827;
}No !important needed
The widget declares its variables on .chainlog-frame and .chainlog-launcher at zero specificity, and your custom CSS loads after them, so any ordinary selector overrides them.
Overriding the brand colour in CSS
The launcher background, header rule, card accent, and top-corner highlight are built from --chainlog-brand and the two tint variables, so they follow your override. The tints themselves are calculated from the brand colour you set in the dashboard or in init, though, so if you change --chainlog-brand in CSS, also set --chainlog-brand-tint and --chainlog-brand-tint-strong as in the example above. Setting brandColor instead updates all three for you.
Custom fonts
Load your own font with an @font-face rule in custom CSS and point --chainlog-font at it.
@font-face {
font-family: "Acme Sans";
src: url("https://cdn.example.com/fonts/acme-sans.woff2") format("woff2");
font-weight: 100 900;
font-display: swap;
}
.chainlog-frame,
.chainlog-launcher {
--chainlog-font: "Acme Sans", system-ui, sans-serif;
}Use @font-face with an https URL, not @import
The widget's Content-Security-Policy allows font files from any https URL, but it blocks remote stylesheets. An @import of a hosted font stylesheet, such as a Google Fonts CSS URL, will not load. Copy the @font-face rules from that stylesheet into your custom CSS instead.
Runtime methods
The runtime surface is intentionally small. Most behavior changes happen through config and entry metadata.
| Method | Purpose |
|---|---|
| init(config) | Create the singleton widget instance and mount the iframe. |
| update(partialConfig) | Merge new config, optionally refresh tokenProvider, and repost config to the frame. |
| destroy() | Remove the iframe instance and clear token refresh timers. |
| open() | Set open state to true for launcher or panel mode. |
| close() | Set open state to false for launcher or panel mode. |
| toggle() | Flip the current open state for launcher or panel mode. |
There is only one widget instance per page
Calling init again updates the existing singleton. Use update for deliberate runtime changes and destroy when unmounting in SPA transitions.
Events and payloads
Use onEvent for analytics, host UI updates, and debugging. Keep handlers resilient because they run in your app shell.
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
tokenProvider,
onEvent(payload) {
if (payload.type === "entry_clicked") {
analytics.track("release_note_opened", {
entryId: payload.entryId,
slug: payload.slug,
deliveryType: payload.deliveryType,
});
}
if (payload.type === "filter_changed") {
console.info("Visible entries", payload.visibleCount);
}
},
});| Event | Common payload fields | When it fires |
|---|---|---|
| launcher_toggle / panel_toggle | open, mode | Fires when the floating surface opens or closes. |
| data_loaded | count, totalLoaded, hasMore, nextCursor, contextModule | Fires on initial load and load more. |
| filter_changed | deliveryFilter, categoryFilter, visibleCount, totalLoaded | Fires when widget filters change. |
| entry_clicked | entryId, slug, title, deliveryType, changeCategory, releaseStatus | Fires when an entry opens inside the widget. |
| load_more_clicked | nextCursor, loadedCount | Fires before the widget fetches the next page. |
| content_height | height | Inline mode only. Lets the host resize the iframe. |
| full_changelog_clicked | url, mode | Fires when a visitor clicks the in-panel 'View full changelog' link. |
| retry_clicked / error | message, mode | Fires on retry attempts and runtime errors. |
Common layouts
The same runtime supports host-owned triggers and inline embeds without a separate SDK.
Host-owned button
<button type="button" onclick="window.ChainlogWidget.toggle()">
Release notes
</button>
<script>
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => fetch("/api/widget-token", { credentials: "include" }).then((r) => r.json()),
mode: "panel",
showTrigger: false,
launcherLabel: "Updates",
});
</script>Inline changelog block
<div id="release-notes"></div>
<script>
window.ChainlogWidget.init({
tenantId: "CHAINLOG_TENANT_ID",
useTenantDefaults: true,
tokenProvider: async () => {
const response = await fetch("/api/widget-token", { credentials: "include" });
return response.json();
},
mode: "inline",
target: "#release-notes",
title: "Release notes",
subtitle: "What changed recently",
autoHeight: true,
});
</script>Errors, token refresh, and sizing behavior
These behaviors are easy to miss when reading code casually, so document them explicitly.
- If you provide tokenProvider and expiry metadata, the loader schedules token refresh before expiry.
- Recoverable auth errors trigger a forced token refresh attempt when tokenProvider is available.
- Inline mode sends content height events and auto-resizes unless you set a fixed height or disable autoHeight.
- Inline mode requires target or a
data-chainlog-widgetcontainer once the page is mounted. - Widget entry fetches currently rate-limit at 120 requests per minute per tenant and widget subject.
- Panel and launcher modes default closed unless you pass
openexplicitly. The loader remembers the visitor's open/closed choice and the last entry they saw inlocalStorage, and only reopens automatically when a genuinely new entry has published since their last visit — it will not reopen on every page load. contextModule(from config or a widget credential's default) is a hard filter, not just a sort hint: entries only show if theirmoduleKeymatches or the value appears in their resurface triggers. A typo'd or mismatched value silently renders an empty "Nothing published yet" state instead of an error — if the widget looks empty, checkcontextModule/credential defaults first.
What to read next