The assistant is styled with CSS custom properties, all named `--tf-*`. Change them and the assistant takes on your colors, corner radius and font. Out of the box it's monochrome: white and near-black in light mode, black and near-white in dark mode.

## How theming works

There are two ways to set the tokens, and they set the same variables:

| Route               | How                                                                                  | Wins over                 |
| ------------------- | ------------------------------------------------------------------------------------ | ------------------------- |
| CSS                 | Set `--tf-*` variables on any ancestor of the assistant in your stylesheet           | The defaults              |
| The `theme` setting | Pass the tokens as an object. They're written inline on the assistant's own elements | Your CSS and the defaults |

Use whichever suits you. CSS works with any Content Security Policy and lets you theme from your existing design tokens. The `theme` setting keeps everything in code and always wins.

How far your page's own CSS reaches into the assistant depends on how you loaded it:

|                    | npm package                                                       | Script tag                                                                      |
| ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Renders in         | Your page's DOM, styled by the package stylesheet                 | A shadow root                                                                   |
| Your CSS selectors | Can reach it. Broad resets or global element styles may affect it | Can't reach it                                                                  |
| `--tf-*` variables | Inherited from ancestors                                          | Inherited from ancestors, through the shadow root                               |
| Sizes              | In `rem`, so they follow your root font size                      | Converted to pixels at 16px per rem, so your root font size doesn't change them |

On either channel, theme through the tokens, not with selectors that target the assistant's internal markup. That markup isn't part of the contract and can change between releases.

## Set tokens in CSS

```css
.app-shell {
  --tf-background: #fdfdfc;
  --tf-foreground: #1c1c1a;
  --tf-primary: #1a3c8b;
  --tf-primary-foreground: #ffffff;
  --tf-radius: 0.375rem;
  --tf-font-sans: "Inter", system-ui, sans-serif;
}
```

Set them on an element that wraps the assistant, such as your app shell or `:root`.

## Set tokens with the theme setting

Pass the same tokens in camelCase. A bare number for `radius` means pixels. The object is compared by value, so creating a new one on every render is fine:

```tsx
<TeaflaskAssistant
  publishableKey="pk_live_…"
  mode="auto"
  theme={{
    background: "#fdfdfc",
    foreground: "#1c1c1a",
    primary: "#1a3c8b",
    primaryForeground: "#ffffff",
    radius: 6,
    fontSans: '"Inter", system-ui, sans-serif',
    dark: {
      background: "#101010",
      foreground: "#f2f2f2",
      primary: "#8fb0ff",
      primaryForeground: "#101010",
    },
  }}/>
```

On the script tag, `theme` is a property on the element and takes the same object.

## The tokens

| Token                                     | Colors                                                                            |
| ----------------------------------------- | --------------------------------------------------------------------------------- |
| `--tf-background`                         | The base surface behind the conversation, the message box and floating controls   |
| `--tf-foreground`                         | Main text and icons                                                               |
| `--tf-card`                               | Approval cards and tool views. Defaults to the background                         |
| `--tf-primary`, `--tf-primary-foreground` | Solid buttons such as Send and Stop, and the text on them                         |
| `--tf-muted`                              | The fill behind the user's own messages, code blocks and the disabled Send button |
| `--tf-muted-foreground`                   | Secondary text, captions and quiet icons                                          |
| `--tf-accent`                             | The hover fill of quiet buttons, menu rows and list rows                          |
| `--tf-destructive`                        | Error text, failure icons and destructive controls                                |
| `--tf-border`                             | Every border and divider                                                          |
| `--tf-input`                              | The message box border and keyboard shortcut hints                                |
| `--tf-ring`                               | The focus outline for keyboard users                                              |

| Token               | Sets                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `--tf-radius`       | Corner radius for buttons, inputs and panels, with some elements derived from it. Defaults to `0.5rem` |
| `--tf-font-sans`    | The font family for all assistant text. Defaults to the system UI font                                 |
| `--tf-color-scheme` | The `color-scheme` that native controls, such as scrollbars and the message box, draw with             |

These tokens are accepted, but no part of the assistant reads them yet: `--tf-card-foreground`, `--tf-popover`, `--tf-popover-foreground`, `--tf-secondary`, `--tf-secondary-foreground` and `--tf-accent-foreground`.

When you change a background, also change the text color that sits on it, and check the contrast.

## Dark mode

| To                                    | Do this                                                                                 |
| ------------------------------------- | --------------------------------------------------------------------------------------- |
| Always use light                      | Leave `mode` unset, or set it to `"light"`                                              |
| Always use dark                       | Set `mode` to `"dark"`                                                                  |
| Follow the visitor's operating system | Set `mode` to `"auto"`                                                                  |
| Follow your app's own theme switch    | Leave `mode` unset and put `data-tf-theme="dark"` on an ancestor while your app is dark |

`mode` is a prop on npm and an attribute on the script tag. When set to `"light"` or `"dark"` it wins over an ancestor's `data-tf-theme`. `"auto"` works through a CSS media query, so the right palette shows from the first paint.

`theme.dark` holds color overrides used only while dark mode is active. A color you leave out of it falls back to the base value you set, and if you set none, to the built-in dark default. `radius`, `fontSans` and `colorScheme` are the same in both modes, so they go on the base object only.

If you build a dark theme from CSS variables alone, with no `mode` and no `data-tf-theme`, also set `--tf-color-scheme: dark`. Otherwise the browser draws scrollbars and the message box for a light page.

## Fonts

`--tf-font-sans` names a font family; it doesn't load one. Load the font the way your app already does, and include a fallback stack. On the script tag, load it in your page. A font loaded in the page is usable inside the shadow root, but your page's `font-family` rules don't reach in. Pass the family through the token.

## Your own mark

The corner widget and the drawer's welcome screen show the Teaflask flask by default. To show your own artwork:

| npm                                                              | Script tag                         |
| ---------------------------------------------------------------- | ---------------------------------- |
| `companionMark`: any React node, such as an `<svg>` or `<img />` | `companion-mark-src`: an image URL |

The mark fills a 4rem square, so a bare `<svg viewBox>` or `<img />` works without sizing. Keep your artwork's proportions inside its viewBox. On the script tag, if the image fails to load, the flask is shown instead, with one console warning. Allow the image's origin in your policy's `img-src`.

The assistant follows the visitor's reduced-motion setting for its own transitions. If your mark is animated, making it respect that setting is up to you.

## Strict Content Security Policy

The `theme` setting writes inline styles. If your policy blocks inline style attributes, theme with CSS variables in your stylesheet instead. You get the same tokens; only the precedence differs.

## More resources

- [Install](/install) – the Content Security Policy entries the assistant needs
- [Surfaces](/surfaces) – the widget, palette and full page that theming applies to
- [Tool views](/tool-views) – styling your own views to match the assistant's palette
