HatchedDocs
Guides

Widget integration

Drop Hatched widgets into any page with one loader script and stable data-hatched-mount attributes.

Widgets are the presentation layer: small Shadow DOM UI pieces that render buddy state directly in your product without leaking CSS in either direction.

The short version

<script
  src="https://cdn.hatched.live/widget.js"
  data-session-token="WIDGET_SESSION_TOKEN"
  data-surface="paper"
  data-geometry="rounded"
  data-motion-profile="standard"
  data-reward-voice="joyful"
  data-iconography="geometric"
  data-typography="sans-serif-modern"
  defer
></script>

<div data-hatched-mount="buddy"></div>
<div data-hatched-mount="badges"></div>
<div data-hatched-mount="streak" data-streak-key="daily_lesson"></div>
<div data-hatched-mount="path"></div>
<div data-hatched-mount="tokens"></div>
<div data-hatched-mount="marketplace"></div>
<div data-hatched-mount="leaderboard"></div>

The loader reads the token from its own script tag, discovers known data-hatched-mount elements, downloads only the bundles present on the page, and keeps them in sync through one shared widget state poller.

The browser global is window.__HATCHED_WIDGET__. If a SPA creates mount elements after the loader has already run, call window.__HATCHED_WIDGET__?.init({ token }) after those elements exist. The required identity field is token; user and buddy identity come from the JWT. Advanced endpoint and theme overrides are documented in Runtime configuration.

From onboarding to production

Onboarding scans the operator's site, extracts a brand brief plus visual identity evidence (palette, typography, motifs), and seeds three widget defaults on the customer: widget_theme_config, widget_custom_css, and widget_size. The Dashboard Widget Studio preview and the install snippets both read those same settings, so the widget a teammate approves in the dashboard is the widget that ships in production.

The shortest path: open Widget Studio → pick a preset (or click AI from theme so the loader rebuilds the theme from your scanned brand evidence) → dial in personality, size, and CSS hooks → Save. Your live page picks up the new theme on next load or focus — see Live theme sync below.

Use the dashboard-generated snippet as the source of truth when possible. It includes the current personality axes, theme variables, custom CSS hook overrides, size, and the correct mount id for each widget. Manual edits should use the public data-* attributes, --hw-* variables, and .hw-* class hooks below so future widget releases can keep the same customization contract.

Live theme sync

The loader fetches /widget/theme on init (using the same widget token you already pass) and on browser focus / tab visibility change. The response carries the customer's current widget_theme_config (preset, personality, vars), widget_custom_css, and widget_size. If anything differs from the inline data-* attributes, the loader hot-swaps the widget — surface, geometry, motion, reward voice, iconography, typography, palette and density all update without a page reload.

What this means in practice:

  • You do not have to re-paste the snippet every time a teammate changes a color or switches a personality dial in Widget Studio.
  • The inline data-surface / data-geometry / data-motion-profile / data-reward-voice / data-iconography / data-typography / data-theme-vars / data-custom-css attributes still ship as fast-paint fallbacks so SSR/no-JS or first-render scenarios get the right widget immediately, but the API answer is authoritative.
  • Theme refresh respects a 5s minimum interval and a 15s Cache-Control on the endpoint so it stays cheap even on heavily-trafficked partner pages.

Theme inheritance precedence

When the same theme attribute is set in more than one place, Hatched resolves in this order — last write wins:

  1. Onboarding scan defaults — the brand brief seeded at signup (palette, typography, motifs). Lowest priority; only used when nothing else is set.
  2. Widget Studio saved theme — what a teammate approved in the dashboard. Persisted on the customer and served by /widget/theme. This is the production answer.
  3. Inline data-* attributes on the loader <script> — fast-paint fallback for SSR / first paint. Overridden by /widget/theme on init, on focus, and on browser tab visibility change.
  4. Per-mount instance identity / viewdata-* attributes on the individual mount <div data-hatched-mount="streak" data-streak-key="daily_lesson">. These carry only instance identity and view selection (data-streak-key, data-path-key, data-display-mode) for that one mount. They do not override personality or theme — personality and theme are read from the loader <script> and /widget/theme only, so this is not a theme precedence level.
  5. Runtime init({ themeVars }) — programmatic overrides passed to window.__HATCHED_WIDGET__.init(...). Highest priority; intended for demo pages, A/B harnesses, embedded preview surfaces.

In short: dashboard is authoritative, inline is fallback, runtime is escape hatch. Production sites typically resolve their theme from levels 1–3; level 4 carries per-mount instance identity (not theme), and level 5 (runtime themeVars) exists for the demo + admin preview surfaces and rarely belongs in a partner page.

Token choice

Use a widget session token for interactive widgets. Mint it on your server with a secret API key:

import { HatchedClient } from '@hatched/sdk-js';

const hatched = new HatchedClient({
  apiKey: process.env.HATCHED_API_KEY!,
});

const session = await hatched.widgetSessions.create({
  buddyId: 'bdy_abc',
  userId: 'user_42',
  scopes: ['read', 'events:track'],
  ttlSeconds: 60 * 15,
});

The read and events:track scopes work on every plan. The marketplace scopes — marketplace:browse, marketplace:purchase, items:equip — are plan-gated: requesting them on a plan without the marketplace capability makes the mint call fail with 403 plan_feature_locked. Add them only for a buddy whose customer is on a marketplace-capable plan:

const shoppingSession = await hatched.widgetSessions.create({
  buddyId: 'bdy_abc',
  userId: 'user_42',
  scopes: ['read', 'events:track', 'marketplace:browse', 'marketplace:purchase', 'items:equip'],
  ttlSeconds: 60 * 15,
});

Use a read-only embed token only when nothing on the page is interactive. The embed token is a short-lived JWT minted by your backend per render, then handed to the browser. It is not an API key and is not created once in the dashboard — every page render mints a fresh one:

Read-only means read-only. The hatch ceremony does not play on an embed token — a player whose buddy is still an egg stays on the egg — and every user write (event tracking, purchases, kudos) returns 403. If the page hosts any interactive widget, use the session-token install above.

// On your server (route handler, server component, etc.)
const embed = await hatched.embedTokens.create({
  buddyId: 'bdy_abc',
  userId: 'user_42',
  ttlSeconds: 60 * 60,
});
// Send embed.token down to the browser.

Then inline that token into the loader script attribute:

<script
  src="https://cdn.hatched.live/widget.js"
  data-embed-token="EMBED_TOKEN"
  defer
></script>
<div data-hatched-mount="buddy"></div>

See Auth model → Embed token for the full lifecycle, raw-HTTP form, and how it differs from a widget session token.

Token matrix

The widget session token is the default install — it covers everything below. The embed column only marks the displays that also work read-only.

Widget / actionToken to use
Hatch ceremony (egg → buddy)Widget session token only — never plays on an embed token
Buddy displayWidget session token; embed token if the buddy is already hatched
Badges displayWidget session token or embed token
Leaderboard displayWidget session token or embed token
Streak displayWidget session token or embed token
Guided path displayWidget session token or embed token
Browser track()Widget session token with events:track
Manual path sub-step completionWidget session token with events:track
Marketplace browseWidget session token with marketplace:browse (embed token shows a read-only catalog)
Marketplace purchaseWidget session token with marketplace:purchase
Equip / unequip itemsWidget session token with items:equip
Kudos send / quest join / box openWidget session token with the matching scope — returns 403 on an embed token

Allowed browser origins

Widget runtime requests are allowed per customer. When onboarding is seeded from a website URL, Hatched automatically adds that URL's origin to settings.widget_allowed_origins. You can add local, staging, and production app origins later in Dashboard → Settings → General → Widget allowed origins. The allowlist is read from customer settings on each widget API request; it is not baked into embed or session tokens.

Use origins only, not full paths:

https://app.example.com
http://localhost:4002

Staging vs production

The loader picks its default API base from the URL it was loaded from:

Loader URLDefault API base
https://cdn.hatched.live/widget.jshttps://api.hatched.live/api/v1
https://cdn.hatched.live/staging/widget.jshttps://api.staging.hatched.live/api/v1

So a snippet that points at the staging CDN automatically talks to the staging API — no data-api-base-url override needed. If you do pass data-api-base-url, the value must be one of the canonical Hatched API origins (api.hatched.live, api.staging.hatched.live); arbitrary values are rejected to prevent token exfiltration via host-page XSS.

Runtime configuration

The script tag is the preferred public configuration surface. The same values can be passed to window.__HATCHED_WIDGET__?.init(...) when a SPA mints or refreshes a token after hydration.

window.__HATCHED_WIDGET__?.init({
  token,
  apiBaseUrl: 'https://api.staging.hatched.live/api/v1',
  cdnBaseUrl: 'https://cdn.hatched.live/staging/widget/v1/',
  themeVars: { '--hw-accent': '#3F8F5F' },
  customCss: '.hw-container { box-shadow: none; }',
  personality: {
    surface: 'paper',
    geometry: 'rounded',
    motion_profile: 'standard',
    reward_voice: 'joyful',
    iconography: 'geometric',
    typography_pair: 'sans-serif-modern',
  },
  size: 'medium',
  lang: 'en',
});
Init keyEquivalent script attributeNotes
tokendata-session-token or data-embed-tokenRequired unless already present on the script tag
apiBaseUrldata-api-base-urlMust be a canonical Hatched API origin
cdnBaseUrlDerived from script srcAdvanced staging / self-hosted bundle override
themeVarsdata-theme-varsJSON object of --hw-* values
customCssdata-custom-css / data-custom-css-idInline CSS or CSS script element
personalitydata-surface, data-geometry, etc.Widget Studio writes these for you
sizedata-sizesmall, medium, or large
langdata-langLocale hint

Per-widget instance identity belongs on the mount element. For example, streak widgets read data-streak-key and path widgets read data-path-key from the <div>, not from init().

Available mounts

Mount attributePurpose
data-hatched-mount="buddy"Animated companion, coins, stage, equipped items
data-hatched-mount="badges"Earned and locked badge shelf
data-hatched-mount="streak"One or more streak counters; add a Dashboard streak key via data-streak-key
data-hatched-mount="path"Guided journey for the audience's active path; add data-path-key to pin a specific path
data-hatched-mount="tokens"Wallet card — spendable balance plus progression-token balances with progress toward each gate
data-hatched-mount="marketplace"Browse, buy, and equip items
data-hatched-mount="leaderboard"Community rank surface
data-hatched-mount="kudos"Peer recognition send + receive
data-hatched-mount="group-quest"Team-scoped quest progress (Growth+)
data-hatched-mount="feed"Passive team event feed
data-hatched-mount="mystery-box"Skinner-box surprise reward (Growth+)
data-hatched-mount="league"Season tier + cohort standing (Growth+)
data-hatched-mount="council"UGC narrative co-authoring (Enterprise)
data-hatched-mount="hexad-survey"Onboarding player-type survey

Legacy ids such as id="buddy-widget" still work for older installs, but new snippets should use data-hatched-mount.

Gated widgets still mount on every plan; the API responds with 403 plan_feature_locked until the customer upgrades, and the widget renders a locked-state placeholder pointing at billing. The error details carry required_plan and an upgrade_url so the placeholder can link straight to the right upgrade. The full plan/capability ground truth lives in Plan capabilities.

Bundle sizes and loading

The 6 KB loader (widget.js) is all your page ships up front. Each widget bundle is fetched on demand the first time its mount is found in the DOM, and the Preact runtime plus shared UI code live in separate cached chunks that every widget reuses — so adding a second or third widget to a page costs only its own entry, not another copy of the framework.

WidgetFirst load (gzip)WidgetFirst load (gzip)
buddy~87 KBpath~49 KB
marketplace~71 KBhexad-survey~48 KB
leaderboard~61 KBgroup-quest~47 KB
badges~56 KBfeed~46 KB
kudos~50 KBstreak~45 KB
tokens~45 KBcouncil~43 KB
league~44 KBmystery-box~41 KB

"First load" is the entry bundle plus the shared chunks it pulls, gzipped — the worst case of a cold cache with that one widget alone on the page. The shared chunk pool is ~65 KB gzipped in total and is downloaded once per page, so a page with three widgets transfers roughly (sum of the three entries) + (the shared pool, once) rather than the sum of three "first load" figures. Repeat visits and incremental deploys reuse content-hashed chunks straight from the browser cache; only a chunk whose contents actually changed is re-fetched.

You don't host or version these files — they're served from cdn.hatched.live, and the loader you embed decides which bundle build to pull (see Widget versioning).

Personality dimensions

Hatched widgets compose six personality axes that decide how the widget feels — not just how it's coloured. The Widget Studio presets pick one value per axis; the loader forwards each as a data-* attribute that drives Shadow-DOM CSS attribute selectors. You can override any single axis without abandoning the preset.

AxisAttributeValuesDrives
Surfacedata-surfacepaper glass metal crt parchment vellumShell background texture (grain, frost, scanlines)
Geometrydata-geometryrounded cut pill sharp organicShell, card, button, badge clip + bar radii
Motion profiledata-motion-profilecalm standard expressive theatricalIdle bounce, hover lift, animation amplitude
Reward voicedata-reward-voicequiet crisp joyful epicCoin pulse, level-up burst, badge glow intensity
Iconographydata-iconographygeometric hand-drawn pixel embossed flat-monoIcon stroke, wobble, pixelation, depth
Typography pairdata-typographysans-serif-modern serif-classic mono-tech rounded-playful display-condensedBody + display font stacks

Pick the combination that matches your brand. A fintech onboarding probably wants vellum + sharp + calm + crisp + geometric + sans-serif-modern. A gaming studio probably wants crt + sharp + theatrical + epic + pixel + mono-tech. A wellness app probably wants parchment + pill + calm + quiet + hand-drawn + serif-classic.

The Widget Studio's AI from theme action and the onboarding scout both populate these axes from your scanned brand evidence, so the preset that ships in production matches the one a teammate approved in the dashboard.

Styling and CSS hooks

Widgets render inside Shadow DOM, so your product CSS cannot accidentally break them. Customization is explicit: pass design tokens on the loader script and use stable .hw-* hooks for deeper polish.

<script
  src="https://cdn.hatched.live/widget.js"
  data-session-token="WIDGET_SESSION_TOKEN"
  data-surface="parchment"
  data-geometry="pill"
  data-motion-profile="calm"
  data-reward-voice="joyful"
  data-iconography="hand-drawn"
  data-typography="serif-classic"
  data-size="medium"
  data-theme-vars='{"--hw-accent":"#3F8F5F","--hw-radius":"24px"}'
  data-custom-css-id="hatched-widget-css"
  defer
></script>

<script type="text/hatched-css" id="hatched-widget-css">
  .hw-container {
    box-shadow: 0 24px 70px -42px rgba(25, 20, 12, 0.42);
  }

  .hw-market-card {
    border-radius: var(--hw-card-radius);
  }
</script>

Loader styling attributes

AttributeValuesPurpose
data-surfacepaper glass metal crt parchment vellumShell background texture
data-geometryrounded cut pill sharp organicCorner radii across shell, cards, buttons
data-motion-profilecalm standard expressive theatricalIdle bounce and hover amplitude
data-reward-voicequiet crisp joyful epicCelebration intensity
data-iconographygeometric hand-drawn pixel embossed flat-monoIcon styling
data-typographysans-serif-modern serif-classic mono-tech rounded-playful display-condensedBody + display font pair
data-sizesmall medium largeWidget density and panel height
data-theme-varsJSON object of --hw-* valuesCSS variables injected into each shadow root
data-custom-cssCSS stringInline custom CSS for short overrides
data-custom-css-idelement idReads CSS from a <script type="text/...">

Prefer variables for colors, radius, spacing mood and shadow. Use custom CSS for component-level details such as a marketplace card hover, badge tile shape or action button treatment.

Supported design tokens

TokenControls
--hw-bgWidget shell background
--hw-bg-elevatedRaised panel background
--hw-surfaceCard and item surfaces
--hw-surface-mutedMuted wells, skeletons, tracks
--hw-borderDefault border color
--hw-border-strongHover and selected border color
--hw-textPrimary text
--hw-text-secondarySecondary text
--hw-text-mutedCaptions and metadata
--hw-accentPrimary action and progress
--hw-accent-strongAction hover and stronger accent
--hw-accent-softPills, glow and soft fills
--hw-positiveSuccess/equipped states
--hw-positive-softSoft success backgrounds
--hw-warnWarning states
--hw-radiusOuter widget radius
--hw-card-radiusCard and item radius
--hw-button-radiusButton radius
--hw-shadowMain widget shadow
--hw-font-bodyBody font stack
--hw-font-displayPlayful/display font stack

Stable class hooks

HookScopeUse for
.hw-containerAll widgetsShell, background, border, radius, shadow
.hw-titleAll widgetsMain heading typography
.hw-cardBuddy, badges, streakShared cards and stat panels
.hw-btnMarketplace, buddyButtons and interactive actions
.hw-market-cardMarketplaceItem tile surface, spacing and hover
.hw-market-assetMarketplaceItem media well
.hw-market-rarityMarketplaceRarity pill
.hw-badge-tileBadges, buddyAchievement tile shape and earned state
.hw-streak-rootStreakStreak panel shell and day strip
.hw-path-rootPathPath panel shell and accent context
.hw-path-nodePathJourney-map and stepper milestone nodes
.hw-path-substepPathSub-step row spacing and state styling
.hw-path-ctaPathManual completion button
.hw-lb-contextLeaderboardCurrent-user rank context
.hw-evo-cardBuddyEvolution stage card and progress rail
.hw-appearance-bannerBuddy, marketplacePending, awaiting credits, and failed appearance states
.hw-modal-noteMarketplacePurchase/equip confirmation helper copy

Do not target generated Preact internals or DOM depth. Hatched treats the hooks above and --hw-* variables as the public customization surface.

Celebrations are API-driven from the loader:

window.__HATCHED_WIDGET__.celebrate({
  kind: 'badge',
  badge: { key: 'week_warrior', label: 'Week Warrior' },
});

Browser event tracking

If the session includes events:track, the host page can send browser-safe events without exposing a secret API key:

await window.__HATCHED_WIDGET__.track('lesson_completed', {
  lessonId: 'lesson_1',
});

The API derives user_id, buddy_id, and audience from the session token, so the browser cannot spoof another user.

Hatching and evolution from events

The widget displays live Hatched state; it does not replace the backend progression loop. events.send() returns effects.evolutionReady === true when the buddy has met the next stage condition. If the customer config has auto_evolve enabled, Hatched starts the evolution operation automatically. If auto_evolve is disabled, your backend should start it explicitly:

const effects = await hatched.events.send({
  eventId,
  userId,
  type: 'lesson_completed',
  properties,
});

if (effects.evolutionReady) {
  const op = await hatched.buddies.evolve(buddyId);
  await hatched.operations.wait(op.operationId);
}

The widget will pick up the new stage through /widget/state after the operation completes. If the buddy has equipped marketplace items, the stage can advance before item compositing is fully recovered. In that case /widget/state returns buddy.appearance.status plus pending_operations; the buddy and marketplace widgets show a non-blocking appearance banner until the composite is ready, awaiting_credits, or failed.

Error states

Every widget renders inside a shared error boundary. When a fetch fails (401/403/429/5xx/network) or a render throws, the boundary swaps the widget for a small themed fallback instead of a blank Shadow DOM:

  • 401 (token invalid/expired) — "Session expired" + a Re-authenticate button. Re-mounting the widget with a fresh data-session-token clears the state.
  • 403 (plan/scope) — "Not available here". Points the user (or your ops team) at upgrading the plan or enabling the capability.
  • 429 (rate-limited) — "Slow down a bit". The widget retries on its own once the rate-limit window passes.
  • 5xx (server) — "Hatched is having trouble". Includes the x-request-id so support can correlate with platform logs.
  • Network failure — "Connection issue" + a Retry button.
  • Unknown render error — generic "Something went wrong"; the stack is logged to the host browser console with the widget name as a prefix.

The fallback is keyboard-accessible (role="alert"), respects var(--hw-*) theme tokens, and shows a short ref: line carrying the request id when present. Don't try to suppress it — the failure state is the customer's signal to act, and the boundary keeps the rest of the page from blowing up.

Common pitfalls

  • Secret key in browser bundle: never do this. Mint session/embed tokens on your backend, or use a publishable key only for read-only SDK calls.
  • Wrong attribute: token attributes live on the script tag (data-session-token or data-embed-token), not on the mount div.
  • Wrong global or init shape: the public global is window.__HATCHED_WIDGET__, and SPA remounts should call window.__HATCHED_WIDGET__?.init({ token }). Do not call window.HatchedWidget?.init({ embedToken, userId }).
  • Missing mount attribute: new snippets should use data-hatched-mount="buddy" (or badges, marketplace, leaderboard, streak, path). Legacy ids such as buddy-widget are still supported.
  • Origin not allowed: /widget/state returning 403 with Origin "... " is not allowed for widget access means the browser origin is missing from Dashboard → Settings → General → Widget allowed origins. Onboarding seeds the pasted website origin automatically, but local/staging app origins may still need to be added. If the origin is visible there and the 403 persists, verify the token was minted for the same Hatched environment and customer you edited; token remint is not required for allowlist changes.
  • Expired token: session tokens should be short-lived. Re-mint on route change, focus, or page refresh.
  • Read-only marketplace token: embed tokens can render catalog, ownership, affordability, and the current outfit. Buying and equipping require a widget session token with marketplace:purchase and items:equip.
  • Appearance pending or failed: equip and evolve can queue an image composite. Watch buddy.appearance.status from /widget/state; for failed with error.code === 'needs_rerender', call POST /widget/appearance/rerender with an items:equip session, wait for ready, then let the user re-equip.