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.

FieldRequiredBehavior
tenantIdRecommendedRequired when using tenant runtime defaults. Identifies which tenant runtime config to fetch.
tokenOptionalShort-lived widget JWT. Use only when you are not using tokenProvider.
tokenProviderOptionalAsync function that returns a token string or { token, expiresInSeconds | expiresAt }.
useTenantDefaultsOptionalWhen 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.
modeOptional, default "launcher"Supported values are "launcher", "panel", and "inline".
showTriggerOptional, default trueHide the built-in trigger when your host app owns the button.
positionOptional, default "bottom-right"Floating position for launcher and panel modes: "bottom-right", "bottom-left", "top-right", or "top-left".
themeOptional, default "light""light", "dark", or "auto". "auto" follows the visitor's system colour scheme and switches live when it changes.
brandColorOptional, default "#0f172a"Accent colour for the launcher, header rule, and card accents. See Appearance options for the accepted formats.
radius / density / font / surface / shadow / cardStyleOptionalDesign options. See Appearance options below for values and defaults.
customCssOptionalA stylesheet injected inside the widget, in both the closed launcher and the open panel. See Custom CSS below.
launcherLabelOptional, default "Product updates"Text on the built-in launcher or panel trigger. Also used as the header title when no title is set.
title / subtitleOptionalOverride the in-widget header copy. Pass subtitle: "" to show no subtitle without hiding the header.
eyebrowOptionalSmall uppercase label shown above the header title. Shows nothing unless you set one.
logoUrlOptionalAn 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.
hideHeaderInline onlyWhen true, removes the header entirely. Ignored in launcher and panel modes.
localeOptional, default "en"Locale used to format entry dates: "en", "es", or "fr". Unsupported values fall back to "en".
maxItemsOptional, default 8How many entries to load per page. Capped at 50.
zIndexOptional, default 2147483000Stacking order of the widget iframe on your page. Lower it if the widget covers your own modals.
width / panelWidth / triggerWidthOptionalDimension overrides for floating layouts. Strings or numbers are supported.
targetInline onlySelector or mount target for inline mode. Required if there is no [data-chainlog-widget] container.
autoHeightInline onlyDefaults to true. Resize messages from the frame adjust height automatically.
contextModuleOptionalPass a product-area trigger such as billing or dashboard to filter visible entries.
changelogUrlOptionalFull changelog URL shown as a link inside the panel. Resolved automatically when useTenantDefaults is true.
openOptionalForce 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.
onEventOptionalReceives 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.

OptionValuesWhat 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.

Presetradius, density, font, surface, shadow, cardStyleLook
Classicround, comfortable, default, gradient, soft, accentSoft gradients, rounded cards, and a brand accent bar. This is the default look.
Minimalsoft, comfortable, system, flat, none, minimalFlat, borderless, and quiet. Lets your content lead.
Boldround, comfortable, default, flat, strong, borderedSolid brand launcher, strong shadows, and outlined cards.
Compactsharp, compact, system, flat, soft, borderedTighter 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.

ClassElement
.chainlog-frameThe widget container in panel and inline modes, and the open launcher panel.
.chainlog-launcherThe closed launcher button.
.chainlog-headerThe header with the title, eyebrow, subtitle, and logo.
.chainlog-browseThe browse and filter bar above the entry list.
.chainlog-entry-cardEach entry card in the list.
.chainlog-entry-titleAn entry's title.
.chainlog-entry-summaryAn entry's summary text.
.chainlog-entry-tagsThe row of tags on an entry card.
.chainlog-footerThe footer below the entry list.
CSS variableControls
--chainlog-brandThe brand colour.
--chainlog-brand-tintA light, translucent tint of the brand colour.
--chainlog-brand-tint-strongA stronger translucent tint of the brand colour.
--chainlog-launcher-bgLauncher background. Built from --chainlog-brand.
--chainlog-launcher-shadowLauncher shadow.
--chainlog-launcher-sheenGlossy highlight over the launcher (none on flat surfaces).
--chainlog-shell-bgBackground of the widget body.
--chainlog-shell-borderOuter border of the widget.
--chainlog-shell-glowInner top highlight of the widget edge.
--chainlog-shell-highlightBrand glow in the top corner. Built from --chainlog-brand-tint.
--chainlog-shell-sheenSoft sheen across the top of the body.
--chainlog-header-bgHeader background.
--chainlog-header-ruleThin line across the top of the header. Built from --chainlog-brand.
--chainlog-logo-ringRing around the header logo.
--chainlog-filter-bgBackground of the browse and filter bar.
--chainlog-section-shadowShadow on the browse bar and on panels such as loading, empty, and entry detail sections.
--chainlog-card-shadowShadow on entry cards.
--chainlog-card-accentAccent bar on entry cards. Built from the brand tints; none unless cardStyle is accent.
--chainlog-card-borderEntry card border colour.
--chainlog-card-border-hoverEntry card border colour on hover.
--chainlog-radius-frameCorner radius of the widget frame.
--chainlog-radius-cardCorner radius of entry cards.
--chainlog-radius-sectionCorner radius of panels such as loading, empty, and entry detail sections.
--chainlog-radius-controlCorner radius of the browse bar and filter selects.
--chainlog-radius-pillCorner radius of buttons, chips, and the launcher.
--chainlog-header-padHeader padding on small screens.
--chainlog-header-pad-smHeader padding on wider screens.
--chainlog-body-padBody padding on small screens.
--chainlog-body-pad-smBody padding on wider screens.
--chainlog-card-padCard padding on small screens.
--chainlog-card-pad-smCard padding on wider screens.
--chainlog-list-gapSpace between entries in the list.
--chainlog-fontFont 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.

MethodPurpose
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);
    }
  },
});
EventCommon payload fieldsWhen it fires
launcher_toggle / panel_toggleopen, modeFires when the floating surface opens or closes.
data_loadedcount, totalLoaded, hasMore, nextCursor, contextModuleFires on initial load and load more.
filter_changeddeliveryFilter, categoryFilter, visibleCount, totalLoadedFires when widget filters change.
entry_clickedentryId, slug, title, deliveryType, changeCategory, releaseStatusFires when an entry opens inside the widget.
load_more_clickednextCursor, loadedCountFires before the widget fetches the next page.
content_heightheightInline mode only. Lets the host resize the iframe.
full_changelog_clickedurl, modeFires when a visitor clicks the in-panel 'View full changelog' link.
retry_clicked / errormessage, modeFires 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-widget container 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 open explicitly. The loader remembers the visitor's open/closed choice and the last entry they saw in localStorage, 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 their moduleKey matches 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, check contextModule/credential defaults first.

What to read next