> For the complete documentation index, see [llms.txt](https://docs.glacislabs.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.glacislabs.com/zero-delta/widget/theming.md).

# Theming

Twenty-three tokens, twenty-two CSS variables, and the isolation rule that makes them the only way in.

Pass a partial theme to any widget, or once to `StableGenieProvider`:

```tsx
import { StableGenieExchange } from "@glacislabs/stablegenie-zerodelta";

<StableGenieExchange
  theme={{ mode: "dark", accent: "#8c9bff", accentText: "#0b0d12", radius: "16px" }}
/>
```

`resolveTheme()` picks `darkTheme` when `mode === "dark"` and `lightTheme` otherwise, then spreads your overrides on top. You only name what you want to change.

> **The `theme` prop is the only way in.** Tokens are emitted as CSS custom properties written **inline on the widget's own root element, never on `:root`**. Page CSS cannot leak into the widget and the widget cannot leak out. A host stylesheet targeting the widget's internal class names is unsupported and will break without warning.

### Tokens

`StableGenieTheme` is flat — 23 fields, all optional when you pass a partial.

#### Colour

| Field        | CSS variable       | Light            | Dark      |
| ------------ | ------------------ | ---------------- | --------- |
| `accent`     | `--sg-accent`      | `#4358e1`        | `#4358e1` |
| `accentText` | `--sg-accent-text` | `#ffffff`        | `#ffffff` |
| `bg`         | `--sg-bg`          | `#ffffff`        | `#0e1115` |
| `surface`    | `--sg-surface`     | `#f7f8fe`        | `#1b1d22` |
| `border`     | `--sg-border`      | `#e8ebfb`        | `#343541` |
| `text`       | `--sg-text`        | `#2c2d34`        | `#ffffff` |
| `textMuted`  | `--sg-text-muted`  | `#959699`        | `#87888a` |
| `info`       | `--sg-info`        | same as `accent` | `#8c9bff` |
| `danger`     | `--sg-danger`      | `#df1b41`        | `#ff5470` |
| `success`    | `--sg-success`     | `#2abd7d`        | `#2abd7d` |

What each colour paints:

* `bg` — the card, and the controls that sit on a tinted pane (the token button, the percent chips, the swap button). In dark mode those controls are `surface` lifted 8 % toward `info`, so they still read as raised.
* `surface` — the tinted **You pay** / **You receive** panes and the From / To tiles.
* `border` — the ring around the detail rows (Compliance, Quote, the rate box) and the wallet chip.
* `text` — every heading and value. Labels and the greyed decimals of an amount are tone steps of this one colour (90, 80, 60 and 35 %), so a host names it once.
* `textMuted` — secondary lines: the chain under a ticker, the dollar line, the balance label.
* `info` — the **Quote** row's icon and its **Best return** pill. Omitted, it follows `accent`. The dark palette's default is a lighter lavender that reads on the dark card; a dark theme that brings its own `accent` and no `info` gets its accent. Set a neutral here when your accent could be read as a verdict — the Emerald and Sand presets use `#292a30`.
* `success` — the compliance shield and its **Passed** pill, and the connected-wallet dot.

#### Shape and type

| Field           | CSS variable          | Default                                                     | Applies to                                  |
| --------------- | --------------------- | ----------------------------------------------------------- | ------------------------------------------- |
| `radius`        | `--sg-radius`         | `24px`                                                      | the outer card, the pill's dialog           |
| `radiusControl` | `--sg-radius-control` | `24px`                                                      | panes, tiles, the detail rows, token button |
| `radiusCta`     | `--sg-radius-cta`     | `24px`                                                      | the primary button and the pill face        |
| `font`          | `--sg-font`           | `'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif` | everything except the amounts               |
| `fontDisplay`   | `--sg-font-display`   | `Benzin,` followed by `font`                                | the amounts                                 |

The small pieces follow the control radius: the percent chips at half of it, the wallet chip and the verdict pills at two thirds.

> **`fontDisplay` names Benzin, but the widget never loads it.** Benzin is the ZeroDelta display face; the widget ships no font file and makes no font request. On a page that already loads Benzin the amounts use it, and everywhere else they fall back to `font`. If you set `font` and leave `fontDisplay` unset, the amounts follow your `font` — a re-branded card is never left with one foreign face.

#### Motion

Identical in light and dark — motion does not theme. Every rule that uses these is `prefers-reduced-motion` guarded, so a user who asks for less motion gets it regardless of what you set.

| Field              | CSS variable             | Default                          | Used for                     |
| ------------------ | ------------------------ | -------------------------------- | ---------------------------- |
| `motionFast`       | `--sg-motion-fast`       | `160ms`                          | small state flips            |
| `motionBase`       | `--sg-motion-base`       | `220ms`                          | expand / collapse, the modal |
| `motionSlow`       | `--sg-motion-slow`       | `320ms`                          | the drawer, the receipt      |
| `motionDeliberate` | `--sg-motion-deliberate` | `480ms`                          | the success sweep            |
| `easeOutSoft`      | `--sg-ease-out-soft`     | `cubic-bezier(0.22,0.61,0.36,1)` | entrances                    |
| `easeInOut`        | `--sg-ease-in-out`       | `cubic-bezier(0.4,0,0.2,1)`      | reversible morphs            |
| `easeSpring`       | `--sg-ease-spring`       | `cubic-bezier(0.34,1.56,0.64,1)` | the ZeroDelta Link pop       |

#### `mode`

`mode` is the only field with no CSS variable. It selects which base palette the other twenty-two are spread over.

### Dark mode is yours to decide

The widget does **not** read `prefers-color-scheme` itself — it renders the mode you give it, so it always matches your page rather than fighting it. Follow the OS setting like this:

```tsx
import { useEffect, useState } from "react";
import { StableGenieExchange } from "@glacislabs/stablegenie-zerodelta";

function useColorScheme() {
  const [dark, setDark] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const sync = () => setDark(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);
  return dark;
}

export function Widget() {
  const dark = useColorScheme();
  return <StableGenieExchange theme={{ mode: dark ? "dark" : "light" }} />;
}
```

### Presets

The five palettes the [widget playground](https://playground.glacislabs.com/) offers. Copy the object you want.

```ts
const light = { mode: "light" };

const dark = { mode: "dark" };

const blackWhite = {
  mode: "light",
  accent: "#292a30", accentText: "#ffffff",
  surface: "#f8f8fa", border: "#e6e7ed",
};

const emerald = {
  mode: "light",
  accent: "#25926e", accentText: "#ffffff",
  surface: "#f6fbf9", border: "#e6e6e7",
  info: "#292a30",
};

const sand = {
  mode: "light",
  accent: "#60473f", accentText: "#ffffff",
  surface: "#f7f6f5", border: "#e6e6e7",
  info: "#292a30",
};
```

Measured contrast of the CTA label on its accent: light and dark 5.6:1, Black/White 14.3:1, Sand 8.5:1 — all WCAG AA. Emerald measures 3.9:1, which meets AA for large text only; darken the accent if your CTA copy must meet AA at 14 px.

### Corners

The three radii move together. The playground drives all three from one slider:

```ts
const corners = (px: number) => ({ radius: `${px}px`, radiusControl: `${px}px`, radiusCta: `${px}px` });

<StableGenieExchange theme={{ ...corners(12) }} />
```

`0` gives a square card; `24` is the default.

### Picking `accentText` for a brand colour

If you set `accent` to your own brand colour, set `accentText` to whichever of black or white is legible on it. This is the helper the playground uses:

```ts
/** Relative luminance (sRGB) — picks black or white CTA text for a custom accent. */
function contrastText(hex: string): string {
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
  if (!m) return "#ffffff";
  const lin = (c: string) => {
    const v = Number.parseInt(c, 16) / 255;
    return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
  };
  const l = 0.2126 * lin(m[1]!) + 0.7152 * lin(m[2]!) + 0.0722 * lin(m[3]!);
  return (l + 0.05) / 0.05 > 1.05 / (l + 0.05) ? "#0b0d12" : "#ffffff";
}

<StableGenieExchange theme={{ accent: brand, accentText: contrastText(brand) }} />
```

### Sizing

The compact card is 380 px wide at most and centres in its container. The wide layout is a 360 px card beside a 280 px **You receive** card, and folds to one column when its container is narrower than 660 px. Set `heightPx` to fix the height and scroll the content inside — useful in a fixed checkout panel. Omit it for fit-content.

```tsx
<StableGenieExchange variant="wide" heightPx={640} />
```

### What you cannot theme

Layout, copy, the ZeroDelta brand mark, the ordering of funding routes, and the success visual — which is accent-tinted by design and never a loud green surface. These are fixed so that a payment surface looks and behaves the same wherever it is embedded.

See every token live, with a copyable snippet, in the [widget playground](https://playground.glacislabs.com/).
