# Theme tokens

> Every CSS variable the widget loader honors — what it does, default value in light and dark mode, and how to override it from the script tag or at runtime.

Source: https://docs.hatched.live/docs/reference/theme-tokens

The widget loader exposes a single contract for visual customization: a
flat set of `--hw-*` CSS custom properties. Every widget reads from these
variables — recolor them once and every mounted widget picks up the new
palette without touching the loader bundle.

This page lists every variable the loader writes, how it maps to the
underlying token system, and how to override it at three different levels.

## How overrides flow

The loader resolves theme tokens in this order, last write wins:

1. **Built-in defaults** — `light` or `dark` palette baked into the loader.
2. **`data-theme-vars`** — JSON-encoded `--hw-*` map on the script tag (set
   once when you paste the embed).
3. **`window.HatchedWidgets.remount({ themeVars })`** — runtime overrides
   pushed from your host page after the embed has mounted. See the
   [Runtime configuration](/docs/guides/widget-integration#runtime-configuration)
   guide for details.

Any key that does **not** begin with `--hw-` is dropped by the loader's
style serializer, so you can't accidentally leak host-page CSS variables
into the shadow DOM. Pass strings only; the loader does not parse JS values.

## Try it

The playground writes overrides into a mock widget styled with the same
tokens as the production loader. Adjust a token to see exactly which
surface it controls, then copy the script tag at the bottom into your
real embed.

<ThemeTokenPlayground />

## Reference

### Surfaces

| Variable | Light | Dark | What it controls |
| --- | --- | --- | --- |
| `--hw-bg` | `#FAF7F2` | `#0D0B08` | Outer widget shell background |
| `--hw-bg-elevated` | `#FEFCF8` | `#1A1814` | Elevated panels (popovers, sheets) |
| `--hw-surface` | `#FFFFFF` | `#2A241A` | Card backgrounds inside the shell |
| `--hw-surface-muted` | `#F4EFE6` | `#1A1814` | Secondary chip / neutral pill background |
| `--hw-overlay` | `rgba(13,11,8,0.48)` | `rgba(0,0,0,0.72)` | Modal scrim |

### Borders

| Variable | Light | Dark | What it controls |
| --- | --- | --- | --- |
| `--hw-border` | `#E0D7C3` | `#3D3629` | Default card and shell border |
| `--hw-border-strong` | `#CFC9BE` | `#5A5246` | Hover/active borders |

### Text

| Variable | Light | Dark | What it controls |
| --- | --- | --- | --- |
| `--hw-text` | `#1A1814` | `#FEFCF8` | Default body text |
| `--hw-text-secondary` | `#5A5246` | `#ECE5D6` | Sub-headings, secondary labels |
| `--hw-text-muted` | `#7E7668` | `#A8A196` | Meta text, captions |
| `--hw-on-accent` | `#FFFFFF` | `#0D0B08` | Text drawn on top of `--hw-accent` |

### Accent (primary brand color)

The accent ramp is what most tenants override first. Pick a brand hue for
`--hw-accent`, derive a darker `--hw-accent-strong` for hover and a tinted
`--hw-accent-soft` for chips/pills.

| Variable | Light | Dark | What it controls |
| --- | --- | --- | --- |
| `--hw-accent` | `#FF6B4A` | `#FF8562` | Primary buttons, key chips, progress fills |
| `--hw-accent-strong` | `#E84F30` | `#FFA284` | Hover, focus, active states |
| `--hw-accent-soft` | `#FFE4D8` | `rgba(255,133,98,0.16)` | Soft fills, badge backgrounds, ghost rows |

### Semantic colors

| Variable | Light | Dark | What it controls |
| --- | --- | --- | --- |
| `--hw-positive` | `#12E8A0` | `#5FFFC6` | Success states, completed progress |
| `--hw-positive-soft` | `#C9FFEC` | `rgba(45,255,185,0.16)` | Success chip backgrounds |
| `--hw-info` | `#6E7BFF` | `#8E99FF` | Informational toasts and tags |
| `--hw-warn` | `#F5A800` | `#FFC427` | Warning ribbons, "at risk" cues |
| `--hw-danger` | `#F84C5C` | `#FF7C87` | Destructive actions, error toasts |
| `--hw-yolk` | `#FFC427` | `#FFC427` | The Hatched egg yolk (rarely overridden) |

### Geometry

| Variable | Default | What it controls |
| --- | --- | --- |
| `--hw-radius` | `16px` | Outer shell radius |
| `--hw-card-radius` | `12px` | Card / panel radius |
| `--hw-button-radius` | `8px` | Buttons, chips |
| `--hw-density-scale` | `1` | Multiplier on vertical rhythm (try `0.85` for compact, `1.15` for relaxed) |
| `--hw-pad-shell` | `16px` | Shell padding |
| `--hw-pad-card` | `12px` | Card padding |
| `--hw-gap` | `12px` | Default flex/grid gap inside widgets |

### Typography

The loader ships **Geist** for body copy and **Gluten** for the playful
display family by default. Override these to inherit the host site's
fonts — the loader injects the fallback chain you provide; it does not
bundle additional webfonts.

| Variable | Default | What it controls |
| --- | --- | --- |
| `--hw-font-body` | `'Geist', 'Inter', system-ui, sans-serif` | Body text |
| `--hw-font-display` | `'Gluten', 'DynaPuff', 'Fraunces', serif` | Headlines, ceremony copy |
| `--hw-font-mono` | `'Geist Mono', 'JetBrains Mono', ui-monospace, monospace` | Code, tokens, numeric counters |
| `--hw-font-base` | `14px` | Body size |
| `--hw-font-title` | `20px` | Section headings |
| `--hw-font-meta` | `12px` | Caption text |
| `--hw-font-eyebrow` | `10px` | Uppercase eyebrows |

### Other geometry primitives

| Variable | Default | What it controls |
| --- | --- | --- |
| `--hw-avatar` | `64px` | Avatar / buddy portrait square |
| `--hw-tile-min` | `140px` | Minimum tile width in marketplace / grid widgets |
| `--hw-bar-h` | `8px` | Progress bar height |
| `--hw-btn-pad-y` | `8px` | Button vertical padding |
| `--hw-btn-pad-x` | `16px` | Button horizontal padding |
| `--hw-shell-max` | `360px` | Widest the shell will grow before clamping |
| `--hw-shadow` | `0 1px 2px rgba(13,11,8,0.04), 0 1px 1px rgba(13,11,8,0.02)` | Card shadow |

### Alias tokens (you don't need to set these)

A handful of widgets reference parallel token names that resolve to the
canonical tokens above. **You never have to set them** — override the
canonical token and the alias follows automatically. They exist only so older
widget styles keep working; listing them here so nothing looks "missing".

| Alias | Resolves to |
| --- | --- |
| `--hw-color-primary` / `--hw-color-primary-strong` / `--hw-color-primary-soft` | `--hw-accent` / `--hw-accent-strong` / `--hw-accent-soft` |
| `--hw-text-primary` / `--hw-text-default` | `--hw-text` |
| `--hw-border-default` / `--hw-border-subtle` | `--hw-border` |
| `--hw-path-accent` | `--hw-accent` (Path widget node ramp) |
| `--hw-radius-card` / `--hw-radius-button` | `--hw-card-radius` / `--hw-button-radius` |
| `--hw-color-text-inverse` / `--hw-color-surface-inverse` | `--hw-bg` / `--hw-text` |

### Optional overrides (no loader default)

These tokens are **not** written by the loader's `:host` block, so they
have no Light/Dark default of their own. They are only read as the first
choice in a `var()` fallback chain — leave them unset and the listed
fallback applies; set them with `data-theme-vars` (or `remount`) to take
control of that one surface.

| Variable | Falls back to | What it controls |
| --- | --- | --- |
| `--hw-bg-sunken` | `--hw-bg` | Inset wells (progress trays, code blocks) |
| `--hw-shell` | `#FEFCF8` (literal) | Chrome layering used by the dressing animation SVG |
| `--hw-skeleton-base` | `--hw-bg-sunken`, then `--hw-bg` | Skeleton placeholder base |
| `--hw-skeleton-shine` | `--hw-bg-elevated`, then `--hw-bg` | Skeleton shimmer highlight |

## Setting tokens from the script tag

The simplest path: pass a `data-theme-vars` attribute on the loader
script. The value is JSON, keys are CSS variable names, values are
strings.

```html
<script
  src="https://cdn.hatched.live/widget.js"
  data-embed-token="EMBED_OR_SESSION_TOKEN"
  data-theme="light"
  data-theme-vars='{"--hw-accent":"#5B5BFF","--hw-accent-strong":"#3F3FFF","--hw-accent-soft":"#E4E4FF","--hw-radius":"20px"}'
  defer
></script>

<div data-hatched-mount="buddy"></div>
```

Keys without the `--hw-` prefix are dropped on parse — there is no way
to leak unrelated host-page variables.

## Setting tokens at runtime

When the host application is an SPA and the embed has already mounted,
push new tokens through the public API:

```ts
window.HatchedWidgets.remount({
  themeVars: {
    '--hw-accent': '#5B5BFF',
    '--hw-accent-strong': '#3F3FFF',
    '--hw-accent-soft': '#E4E4FF',
  },
});
```

`remount()` re-runs mount discovery and updates the in-memory config —
already-mounted widgets receive the new tokens via their shadow-DOM style
elements, no flicker.

## Tips

- **Light + dark with one accent ramp.** Override `--hw-accent`,
  `--hw-accent-strong`, and `--hw-accent-soft` once with `data-theme-vars`
  and the loader keeps the rest of the palette in sync with `data-theme`.
- **Densify a sidebar embed.** Set `--hw-density-scale: 0.85` to drop the
  vertical rhythm without rewriting every widget's spacing.
- **Match host fonts.** Override `--hw-font-body` and `--hw-font-display`
  with the host site's fallback chain. The loader does not load
  additional webfonts — the host page is responsible for serving them.
- **Round buttons but square cards.** Tokens are independent: pick
  `--hw-button-radius: 9999px` for pill buttons while keeping
  `--hw-card-radius: 8px`.

## Companion docs

- [Widget integration guide](/docs/guides/widget-integration) — how to
  paste the embed, choose a mode, and respond to events.
- [Widget reference](/docs/reference/widgets/buddy) — every widget and
  the `data-*` attributes it accepts.
- [`@hatched/widgets-loader-types`](https://www.npmjs.com/package/@hatched/widgets-loader-types)
  — IDE autocomplete for `themeVars` and `window.HatchedWidgets`.
