The picture
The same Vendo overlay, in two products. Same markup, same components, different token file.
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 reads them..vendo/theme.json
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:
Where it comes from
You do not write it from scratch. You inherit it, then change what you want.vendo init reads your app’s own CSS and writes the file. It then prints the
paste that imports it into your provider:
app/layout.tsx
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.
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.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 owntheme prop.
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.
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.Instructions
The prose your agent reads before every turn.
Context
Who is asking, and what their screen shows.
Knowledge
Answers from your docs, with citations.
Token reference
Every value the five groups expand into. Open a panel when you need an exact number.Density variables
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.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.Color scheme
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.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:Motion variables
Motion variables
motion: "reduced" collapses the duration to 0ms. The easing curve does not
change.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.Mobile takeover
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 Keep the
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:0px fallback. Outside the takeover the variable is not set at all,
and with the takeover open but no keyboard showing it is 0px.