interactive-ui · v1 · 2026-10-17 · sha256 9c60ff44db235dd9

interactive-ui v1A

Immutable. This exact content never changes and is served at /api/v1/blob/9c60ff44db235dd9.

---
name: interactive-ui
author: vizier
description: Use before calling ui_render_interactive_ui (or any time you draw an HTML page, dashboard, chart, table or form in the WebUI) so the page matches the WebUI theme and fits the reply.
keywords:
- ui
- html
- page
- dashboard
- chart
- table
- form
- interactive
- render_interactive_ui
version: 1
---

# Interactive pages in the WebUI

`ui_render_interactive_ui(title=…, html=…)` draws a page inside your reply. It is in the `ui`
category and only exists while the person is on the WebUI. This skill is how to make that page
look like part of the WebUI rather than a bare web page.

## 1. The page is already themed: write plain HTML first

Every page is drawn with a stylesheet that matches the WebUI's light or dark theme. Before you
write any CSS, use what is already there:

- **Styled elements**: `h1`–`h4`, `p`, `a`, `small`, `ul`/`ol`, `table`/`th`/`td`, `code`, `pre`,
  `kbd`, `blockquote`, `hr`, `button`, `input`, `select`, `textarea`, `label`, `fieldset`,
  `details`/`summary`, `progress`, `meter`.
- **Layout classes**: `.card` (surface box with border and 8px corners), `.grid` (responsive
  columns, 180px minimum), `.row` (wrapping flex row with gaps), `.stack` (column with gaps),
  `.scroll` (horizontal scroll, for wide tables).
- **Content classes**: `.stat` (`<b>value</b><span>label</span>`), `.badge`, plus `.accent`,
  `.ok`, `.warn` or `.err`, `.bar` (track, with an inner `<span style="width:40%">`),
  `button.primary`, `.muted`, `.faint`, `.num` (tabular figures, right-aligned).
- **Variables**: `--text --muted --faint` (ink), `--surface --border --tint` (surfaces and
  subtle fills), `--accent --accent-soft --accent-text`, status `--ok --warn --error` with
  `--ok-bg --warn-bg --error-bg`, charts `--series-1` … `--series-6`, and `--radius --shadow
  --font --mono`.

The kit has zero specificity, so any rule you write wins. Add CSS only for what the kit lacks
(a chart's geometry, a custom layout), and build it from the variables, never from colours you
pick yourself. Hard-coded colours are what make a page look foreign, and they break in the
other theme.

A complete page can be this small:

```html
<h3>Release status</h3>
<div class="grid">
  <div class="card stat"><b>12</b><span>Merged this week</span></div>
  <div class="card stat"><b>3</b><span>Waiting on review</span></div>
  <div class="card stat"><b>1</b><span>Failing CI</span></div>
</div>
<table>
  <thead><tr><th>PR</th><th>Owner</th><th>State</th></tr></thead>
  <tbody>
    <tr><td>#212 Usage export</td><td>ana</td><td><span class="badge ok">merged</span></td></tr>
    <tr><td>#215 Retry budget</td><td>li</td><td><span class="badge warn">review</span></td></tr>
  </tbody>
</table>
```

## 2. Design for where it is drawn

- **It sits in a chat bubble.** Often 500–700px wide, sometimes a phone. No fixed widths, no
  full-page hero sections, no 100vh layouts. Use `.grid` or `flex-wrap` so things reflow, and
  wrap wide tables in `.scroll`.
- **Leave the background transparent.** The reply already sits on the WebUI background; use
  `.card` for boxes. Never set `body` or `html` backgrounds.
- **One point per page.** Lead with the answer (a stat, a headline row), then the detail. Your
  reply text carries the explanation, so the page does not need paragraphs of prose.
- **Compact, not cramped.** The kit's spacing matches the WebUI; don't add large paddings or
  font sizes over 1.6em.
- **Both themes.** If you write any colour at all, use a variable. Check that text on any fill
  you made still reads in both light and dark.

## 3. Charts

There is no network, so no chart libraries: draw with inline SVG (or HTML bars with `.bar`).

- Series colours are `var(--series-1)` … `var(--series-6)`, **in that order**, assigned to
  each entity once so a filter never repaints them. Six is the limit; fold the rest into
  "Other".
- SVG text is black by default and disappears in dark mode: put `fill="currentColor"` on the
  `<svg>` or the `<text>`, and use `var(--muted)` for axis labels.
- Grid lines `stroke="var(--border)"`, thin marks (2px lines), no dual axes.
- For two or more series, show a legend and label the marks directly where there is room. Some
  series colours are low-contrast on the light theme, so a value should never be readable only
  through its colour.
- Use `viewBox` with `width="100%"` so the chart scales with the bubble. Position the points
  of a dense strip or timeline (50+ items) absolutely in percentages instead of with a
  `repeat(N, 1fr)` grid, which won't shrink below its content and overflows.
- Hovering a mark should show its value (a `<title>` inside the SVG element is enough).
- A number is often better as a `.stat` than as a chart with a single bar.

## 4. Making it interactive

The page's script gets `vizier`:

- `vizier.send(text, data?)` posts `text` as the person's message and starts a turn. Use it for
  a real choice the person makes (pick an option, submit a form). It only works after they
  interacted with the page, at most once a second. Make `text` read naturally in the chat ("Pick
  plan B"), and put the machine-readable part in `data`.
- `vizier.saveState(value)` / `vizier.state` / `vizier.onState(cb)` remember UI state (a
  selected tab, a checked list) without a turn. On load, redraw from `vizier.state` if it is not
  `null`, so a page reopened later looks the way the person left it.
- Use real `<button>`s and `<label>`s around inputs, so the page works with a keyboard and a
  screen reader.

## 5. Building big pages

- The code-mode sandbox refuses one long string literal: a multi-line `"""…"""` of a couple of
  KB (about 150 lines) can already fail with `SyntaxError: Source is too deeply nested`. Build a
  large page from chunks of about 2 KB, e.g. `html = "".join([head, style, body, script])`,
  rather than one huge literal.
- Prefix your own class names per component (`.timeline-tick`, not `.tick` or `.rc`). A short
  generic name can collide with another rule and blow up a layout in a way that's hard to trace.
- Keep the page under 256 KB. Images only as `data:` URLs, and only small ones.
- Call the tool once per reply, with the final page. A second call replaces the first.

## 6. Check before you send

- If the page has logic (state, calculations, a game), run that logic first: parse the script
  (`new Function(code)` under node, if you have a shell with node) and drive its functions with
  a stubbed `document`/`vizier`.
- If you have a shell with a headless Chromium-based browser, render the page and measure it:
  `--dump-dom` with an appended script that reports `document.documentElement.scrollWidth`
  against `clientWidth`, and any element whose `getBoundingClientRect().right` exceeds the
  width, at about 500px and 1000px. Test 500px, not 375px: headless window sizes clamp.
- If you have neither, re-read the HTML once for fixed widths, hard-coded colours and missing
  `fill="currentColor"`; those three cause most broken pages.

## 7. When it says `deferred`

The tool returns `{"status": "deferred"}` when no WebUI tab is open. The page appears the next
time the person opens the conversation, so tell them that instead of saying it is on screen.