> ## Documentation Index
> Fetch the complete documentation index at: https://vendo-mintlify-9465070f.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Theming

> One token file sets the colors, type, radii, density, and motion of every Vendo surface in your product.

Your brand, extracted from your own UI on day one, then yours to edit.

## The picture

The same Vendo overlay, in two products. Same markup, same components, different token file.

| Token                  | Maple         | A dark brand |
| ---------------------- | ------------- | ------------ |
| `colors.accent`        | `#111111`     | `#8b5cf6`    |
| `colors.background`    | `#FBFBFA`     | `#14151a`    |
| `radius.medium`        | `14px`        | `5px`        |
| `density`              | `comfortable` | `compact`    |
| `motion`               | `full`        | `reduced`    |
| `--vendo-color-scheme` | `light`       | `dark`       |

Nothing in the chrome hardcodes a Vendo brand. A dark background flips the
whole surface on its own.

That last row is derived, not written. Vendo takes the relative luminance of
`colors.background`, flips to `dark` below `0.179`, and the chrome's
`light-dark()` branches follow.

## The token file

Five groups, one file. Every shipped surface and every
[prewired primitive](/generated/host-components) reads them.

```json .vendo/theme.json focus={2,12,13,14,15} theme={null}
{
  "colors": {
    "background": "#FBFBFA",
    "surface": "#FFFFFF",
    "text": "#111111",
    "muted": "#908C85",
    "accent": "#111111",
    "accentText": "#FFFFFF",
    "danger": "#B42318",
    "border": "#ecebe8"
  },
  "typography": { "fontFamily": "Inter, sans-serif", "baseSize": "15px" },
  "radius": { "small": "6px", "medium": "14px", "large": "14px" },
  "density": "comfortable",
  "motion": "full"
}
```

| Group        | What it drives                                                   |
| ------------ | ---------------------------------------------------------------- |
| `colors`     | surfaces, text, accents, borders                                 |
| `typography` | the body font, and the size the whole type scale is derived from |
| `radius`     | every corner in the chrome                                       |
| `density`    | control heights, padding, and gaps                               |
| `motion`     | `160ms`, or `0ms` when you set it to `reduced`                   |

Those five groups are the whole required shape. Every token added since is
optional, so a theme file written a year ago still parses today.

These land as `--vendo-*` CSS variables on the surface root. Read them from
your own styles and a custom panel tracks the same theme:

```css theme={null}
.my-card {
  padding: var(--vendo-density-card-padding);
  transition:
    background-color var(--vendo-motion-duration)
    var(--vendo-motion-easing);
}
```

## Where it comes from

You do not write it from scratch. You inherit it, then change what you want.

<svg viewBox="0 0 760 118" role="img" aria-label="vendo init reads your live UI, writes .vendo/theme.json into your repo, and you edit it like any other file" style={{ width: "100%", height: "auto", margin: "1.5rem 0" }}>
  <g fill="none" stroke="currentColor" strokeOpacity="0.2">
    <rect x="1" y="1" width="226" height="52" rx="11" />

    <rect x="533" y="1" width="226" height="52" rx="11" />
  </g>

  <rect x="267" y="1" width="226" height="52" rx="11" fill="#6c3bff" fillOpacity="0.06" stroke="#6c3bff" strokeOpacity="0.45" />

  <g fill="none" stroke="#6c3bff" strokeWidth="1.5" strokeLinecap="round">
    <path d="M235 27h20" />

    <path d="M501 27h20" />
  </g>

  <g fill="#6c3bff">
    <path d="M255 23l7 4-7 4z" />

    <path d="M521 23l7 4-7 4z" />
  </g>

  <g fill="currentColor" fontSize="13.5" fontWeight="600" textAnchor="middle">
    <text x="114" y="32">vendo init</text>
    <text x="646" y="32">You</text>
  </g>

  <text x="380" y="32" fill="#6c3bff" fontSize="13.5" fontWeight="600" textAnchor="middle">.vendo/theme.json</text>

  <g fill="currentColor" fillOpacity="0.55" fontSize="11.5" textAnchor="middle">
    <text x="114" y="76">reads your live UI</text>
    <text x="380" y="76">written into your repo</text>
    <text x="646" y="76">edit it like any other file</text>
  </g>
</svg>

`vendo init` reads your app's own CSS and writes the file. It then prints the
paste that imports it into your provider:

```tsx app/layout.tsx focus={5} theme={null}
import theme from "../.vendo/theme.json";
import type { VendoTheme } from "@vendoai/vendo";
import { VendoProvider } from "@vendoai/vendo/react";

<VendoProvider baseUrl="/api/vendo" theme={theme as VendoTheme}>
  {children}
</VendoProvider>
```

The prop takes a partial theme and merges it group by group over Vendo's
neutral default. Pass the file, an inline object, or the file with a few keys
spread on top.

Later runs of `vendo sync` re-read your app and update the slots you have not
touched. A slot you edited by hand is pinned and reported rather than
overwritten, and `vendo sync --theme-refresh` takes your app's value back.

<Note>
  `createVendo` has its own `theme` key, resolved once at boot. It feeds app
  generation and the one theme line in the agent's prompt, not the chrome.
  Leave it unset and the server reads `.vendo/theme.json`.
</Note>

## One surface, a different theme

The provider sets the brand for everything under it. A single surface can
differ — a dark assistant panel on a light page, a compact slot in a dense
console — with its own `theme` prop.

```tsx theme={null}
import { VendoOverlay, VendoSlot } from "@vendoai/vendo/react";

<VendoSlot id="net-worth-card" />
<VendoSlot id="ops-console" theme={{ density: "compact" }} />
<VendoOverlay theme={darkPanel} />
```

It merges over the provider's resolved theme group by group, the same way the
provider's merges over Vendo's default — so a surface states only what differs,
and the groups it leaves out stay the brand. With no provider above it, the
merge runs over the default.

Six surfaces take it: `VendoOverlay`, `VendoSlot`, `VendoTrigger`,
`VendoAppEmbed`, `VendoApprovalEmbed`, and `VendoToolResult`. Type it with
`Partial<VendoTheme>`.

What a surface sends to `document.body` goes with it. The overlay panel, the
approval modal a press parks on, and the toast stack all wear the theme of the
surface they came from rather than falling back to the provider's.

<Warning>
  A surface theme styles Vendo's **frame**, not the view inside it. A generated
  view keeps the **provider** theme either way it mounts — served in an iframe,
  or rendered natively as a pin — because the view is its own theme boundary and
  restates the provider's tokens on itself. Theme a surface to change the chrome
  around a generated view; change the provider to change the view.
</Warning>

A slot showing your own markup renders it untouched, with no Vendo wrapper at
all. There is no chrome on screen there, so `theme` has nothing to style.

## Where to go next

Theme is how it looks. These three are what it knows.

<Columns cols={3}>
  <Card title="Instructions" icon="file-pen" href="/customize/instructions">
    The prose your agent reads before every turn.
  </Card>

  <Card title="Context" icon="user" href="/customize/context">
    Who is asking, and what their screen shows.
  </Card>

  <Card title="Knowledge" icon="book-open" href="/customize/knowledge">
    Answers from your docs, with citations.
  </Card>
</Columns>

## Token reference

Every value the five groups expand into. Open a panel when you need an exact
number.

<AccordionGroup>
  <Accordion title="Density variables">
    `density: "compact"` tightens control sizes, padding, and gaps, and
    `comfortable` relaxes them. Every compact value is less than or equal to its
    comfortable counterpart.

    | Variable                          | `compact`  | `comfortable` |
    | --------------------------------- | ---------- | ------------- |
    | `--vendo-density`                 | `compact`  | `comfortable` |
    | `--vendo-density-control-height`  | `32px`     | `38px`        |
    | `--vendo-density-control-padding` | `6px 10px` | `9px 12px`    |
    | `--vendo-density-card-padding`    | `12px`     | `16px`        |
    | `--vendo-density-content-gap`     | `7px`      | `10px`        |
    | `--vendo-density-inline-gap`      | `5px`      | `7px`         |
    | `--vendo-density-field-gap`       | `4px`      | `6px`         |
    | `--vendo-density-table-padding`   | `7px 10px` | `10px 12px`   |
    | `--vendo-density-badge-height`    | `20px`     | `24px`        |
    | `--vendo-density-badge-padding`   | `3px 7px`  | `5px 9px`     |
    | `--vendo-density-stat-padding`    | `9px 11px` | `12px 14px`   |
    | `--vendo-density-tabs-padding`    | `3px`      | `4px`         |
    | `--vendo-density-tab-height`      | `26px`     | `30px`        |
    | `--vendo-density-tab-padding`     | `4px 8px`  | `6px 10px`    |

    A Kit container takes its own `density` adjective and re-emits this same
    ladder on its own element. That is how a compact table sits inside a
    comfortable page.
  </Accordion>

  <Accordion title="Color scheme">
    Vendo derives `color-scheme` from the WCAG relative luminance of
    `colors.background` and publishes it as `--vendo-color-scheme`. Backgrounds
    below `0.179` resolve to `dark`, and everything else resolves to `light`.

    | Variable               | Light background | Dark background |
    | ---------------------- | ---------------- | --------------- |
    | `--vendo-color-scheme` | `light`          | `dark`          |

    A color the parser cannot read resolves to `light`. Only `#rgb`, `#rgba`,
    `#rrggbb`, and `#rrggbbaa` are parsed.

    The chrome's `light-dark()` branches key off this variable, so a dark-brand
    host gets dark chrome with no extra configuration. Read it from your own
    styles to make a custom surface track the same flip:

    ```css theme={null}
    .my-card {
      color-scheme: var(--vendo-color-scheme);
    }
    ```
  </Accordion>

  <Accordion title="Motion variables">
    `motion: "reduced"` collapses the duration to `0ms`. The easing curve does not
    change.

    | Variable                  | `full`                           | `reduced`                        |
    | ------------------------- | -------------------------------- | -------------------------------- |
    | `--vendo-motion`          | `full`                           | `reduced`                        |
    | `--vendo-motion-duration` | `160ms`                          | `0ms`                            |
    | `--vendo-motion-easing`   | `cubic-bezier(0.2, 0.8, 0.2, 1)` | `cubic-bezier(0.2, 0.8, 0.2, 1)` |

    Both defaults are overridable. Set `motionDuration` or `motionEasing` on the
    theme and `full` motion uses your values instead, while `reduced` still pins
    the duration to `0ms`.
  </Accordion>

  <Accordion title="Mobile takeover">
    The overlay panel goes full-bleed at viewports 767px wide and narrower. It
    covers the host layout rather than docking inside a squeezed column.

    The panel portals to `document.body`, so a transformed or filtered host
    ancestor cannot confine it. It pads all four edges with the iOS safe-area
    insets.

    Touch ergonomics are keyed to small viewports **or** coarse pointers, so a
    tablet in a wide orientation still gets honest targets. The composer and the
    palette search render at 16px to stop iOS auto-zoom, and icon buttons and the
    overlay close control expand to 44px.

    Desktop chrome is unchanged, and the palette keeps its own presentation at
    every width.

    While the takeover is open, the panel stamps a `--fl-kb-inset` variable
    derived from `visualViewport` so the composer lifts above the on-screen
    keyboard. Read it if you render a custom surface inside one:

    ```css theme={null}
    .my-mobile-footer {
      padding-bottom: calc(
        env(safe-area-inset-bottom, 0px) + var(--fl-kb-inset, 0px)
      );
    }
    ```

    Keep the `0px` fallback. Outside the takeover the variable is not set at all,
    and with the takeover open but no keyboard showing it is `0px`.
  </Accordion>
</AccordionGroup>
