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-cssattributes 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-Controlon the endpoint so it stays cheap even on heavily-trafficked partner pages.
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', 'marketplace:browse', 'marketplace:purchase', 'items:equip'],
ttlSeconds: 60 * 15,
});Use a read-only embed token when the page only displays state. 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:
// 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
| Widget / action | Token to use |
|---|---|
| Buddy display | Embed token or widget session token |
| Badges display | Embed token or widget session token |
| Leaderboard display | Embed token or widget session token |
| Streak display | Embed token or widget session token |
| Guided path display | Embed token or widget session token |
Browser track() | Widget session token with events:track |
| Manual path sub-step completion | Widget session token with events:track |
| Marketplace browse | Embed/session token; add marketplace:browse for session review |
| Marketplace purchase | Widget session token with marketplace:purchase |
| Equip / unequip items | Widget session token with items:equip |
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:4002Staging vs production
The loader picks its default API base from the URL it was loaded from:
| Loader URL | Default API base |
|---|---|
https://cdn.hatched.live/widget.js | https://api.hatched.live/api/v1 |
https://cdn.hatched.live/staging/widget.js | https://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 key | Equivalent script attribute | Notes |
|---|---|---|
token | data-session-token or data-embed-token | Required unless already present on the script tag |
apiBaseUrl | data-api-base-url | Must be a canonical Hatched API origin |
cdnBaseUrl | Derived from script src | Advanced staging / self-hosted bundle override |
themeVars | data-theme-vars | JSON object of --hw-* values |
customCss | data-custom-css / data-custom-css-id | Inline CSS or CSS script element |
personality | data-surface, data-geometry, etc. | Widget Studio writes these for you |
size | data-size | small, medium, or large |
lang | data-lang | Locale 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 attribute | Purpose |
|---|---|
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 |
Legacy ids such as id="buddy-widget" still work for older installs, but new
snippets should use data-hatched-mount.
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.
| Axis | Attribute | Values | Drives |
|---|---|---|---|
| Surface | data-surface | paper glass metal crt parchment vellum | Shell background texture (grain, frost, scanlines) |
| Geometry | data-geometry | rounded cut pill sharp organic | Shell, card, button, badge clip + bar radii |
| Motion profile | data-motion-profile | calm standard expressive theatrical | Idle bounce, hover lift, animation amplitude |
| Reward voice | data-reward-voice | quiet crisp joyful epic | Coin pulse, level-up burst, badge glow intensity |
| Iconography | data-iconography | geometric hand-drawn pixel embossed flat-mono | Icon stroke, wobble, pixelation, depth |
| Typography pair | data-typography | sans-serif-modern serif-classic mono-tech rounded-playful display-condensed | Body + 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
| Attribute | Values | Purpose |
|---|---|---|
data-surface | paper glass metal crt parchment vellum | Shell background texture |
data-geometry | rounded cut pill sharp organic | Corner radii across shell, cards, buttons |
data-motion-profile | calm standard expressive theatrical | Idle bounce and hover amplitude |
data-reward-voice | quiet crisp joyful epic | Celebration intensity |
data-iconography | geometric hand-drawn pixel embossed flat-mono | Icon styling |
data-typography | sans-serif-modern serif-classic mono-tech rounded-playful display-condensed | Body + display font pair |
data-size | small medium large | Widget density and panel height |
data-theme-vars | JSON object of --hw-* values | CSS variables injected into each shadow root |
data-custom-css | CSS string | Inline custom CSS for short overrides |
data-custom-css-id | element id | Reads 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
| Token | Controls |
|---|---|
--hw-bg | Widget shell background |
--hw-bg-elevated | Raised panel background |
--hw-surface | Card and item surfaces |
--hw-surface-muted | Muted wells, skeletons, tracks |
--hw-border | Default border color |
--hw-border-strong | Hover and selected border color |
--hw-text | Primary text |
--hw-text-secondary | Secondary text |
--hw-text-muted | Captions and metadata |
--hw-accent | Primary action and progress |
--hw-accent-strong | Action hover and stronger accent |
--hw-accent-soft | Pills, glow and soft fills |
--hw-positive | Success/equipped states |
--hw-positive-soft | Soft success backgrounds |
--hw-warn | Warning states |
--hw-radius | Outer widget radius |
--hw-card-radius | Card and item radius |
--hw-button-radius | Button radius |
--hw-shadow | Main widget shadow |
--hw-font-body | Body font stack |
--hw-font-display | Playful/display font stack |
Stable class hooks
| Hook | Scope | Use for |
|---|---|---|
.hw-container | All widgets | Shell, background, border, radius, shadow |
.hw-title | All widgets | Main heading typography |
.hw-card | Buddy, badges, streak | Shared cards and stat panels |
.hw-btn | Marketplace, buddy | Buttons and interactive actions |
.hw-market-card | Marketplace | Item tile surface, spacing and hover |
.hw-market-asset | Marketplace | Item media well |
.hw-market-rarity | Marketplace | Rarity pill |
.hw-badge-tile | Badges, buddy | Achievement tile shape and earned state |
.hw-streak-root | Streak | Streak panel shell and day strip |
.hw-path-root | Path | Path panel shell and accent context |
.hw-path-node | Path | Journey-map and stepper milestone nodes |
.hw-path-substep | Path | Sub-step row spacing and state styling |
.hw-path-cta | Path | Manual completion button |
.hw-lb-context | Leaderboard | Current-user rank context |
.hw-evo-card | Buddy | Evolution stage card and progress rail |
.hw-appearance-banner | Buddy, marketplace | Pending, awaiting credits, and failed appearance states |
.hw-modal-note | Marketplace | Purchase/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.
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-tokenordata-embed-token), not on the mount div. - Wrong global or init shape: the public global is
window.__HATCHED_WIDGET__, and SPA remounts should callwindow.__HATCHED_WIDGET__?.init({ token }). Do not callwindow.HatchedWidget?.init({ embedToken, userId }). - Missing mount attribute: new snippets should use
data-hatched-mount="buddy"(orbadges,marketplace,leaderboard,streak,path). Legacy ids such asbuddy-widgetare still supported. - Origin not allowed:
/widget/statereturning 403 withOrigin "... " is not allowed for widget accessmeans 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:purchaseanditems:equip. - Appearance pending or failed: equip and evolve can queue an image
composite. Watch
buddy.appearance.statusfrom/widget/state; forfailedwitherror.code === 'needs_rerender', callPOST /widget/appearance/rerenderwith anitems:equipsession, wait forready, then let the user re-equip.