branch:
visuals.md
10078 bytesRaw
# Visuals & UI
## Design system: Kumo
The playground (and eventually all examples) uses [Kumo](https://kumo-ui.com/), Cloudflare's internal design system (`@cloudflare/kumo`). It gives us semantic color tokens, accessible components, and automatic light/dark mode — all without maintaining our own component primitives.
### Setup
- **Package**: `@cloudflare/kumo` (installed at monorepo root as a devDependency)
- **Icons**: `@phosphor-icons/react` v2 (Kumo's peer icon library, also at root). Always use the `*Icon` suffixed exports (e.g. `TrashIcon`, `ShieldIcon`) — the bare names (`Trash`, `Shield`) are deprecated.
- **Shared UI**: `@cloudflare/agents-ui` (private workspace package at `packages/agents-ui/`) — ships the Workers color theme CSS, shared React components (`ConnectionIndicator`, `ModeToggle`), and hooks (`ThemeProvider`, `useTheme`)
- **Tailwind v4**: Requires `@tailwindcss/vite` in `vite.config.ts` (alongside `@vitejs/plugin-react` and `@cloudflare/vite-plugin`). Kumo ships its own Tailwind plugin; imported in `styles.css`:
```css
@source "../../../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}";
@import "tailwindcss";
@import "@cloudflare/kumo/styles/tailwind";
@import "@cloudflare/agents-ui/theme/workers.css";
```
Note: the `@source` path must point to the hoisted Kumo package at the monorepo root (`../../../node_modules`), not a local `node_modules`.
### Dark mode
Kumo uses a `data-mode` attribute on `<html>` (not Tailwind's `dark:` class variant). Our `useTheme` hook sets `document.documentElement.setAttribute("data-mode", resolved)`. All Kumo semantic tokens (`bg-kumo-base`, `text-kumo-default`, `border-kumo-line`, etc.) respond to this automatically — no `dark:` prefixes anywhere in the codebase.
### Color themes
Kumo supports theming via a `data-theme` attribute on a parent element. Themes override semantic token values while keeping the same token names, so components adapt automatically.
We ship a **Workers** theme (via `@cloudflare/agents-ui`) inspired by [workers.cloudflare.com](https://workers.cloudflare.com/):
- **Brand**: Cloudflare orange (`#f6821f`) for primary actions and links
- **Dark mode**: Deep navy-black surfaces (`#08090d` base) with subtle blue-tinted grays
- **Light mode**: Clean white base with crisp structural borders
The theme lives in `packages/agents-ui/src/theme/workers.css` and is consumed via:
```css
@import "@cloudflare/agents-ui/theme/workers.css";
```
Then set `data-theme="workers"` on `<html>`.
### Shared components and hooks — always use `@cloudflare/agents-ui` first
Before building any UI that handles connection status, theme switching, or branding, check `@cloudflare/agents-ui` — it likely already has what you need. The goal is zero hand-rolled duplicates of these patterns across examples.
`@cloudflare/agents-ui` exports:
| Export | Import path | Purpose |
| --------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `ConnectionIndicator` | `@cloudflare/agents-ui` | Dot + label for WebSocket connection state (`connecting`, `connected`, `disconnected`) |
| `ModeToggle` | `@cloudflare/agents-ui` | Button cycling system → light → dark theme modes |
| `PoweredByAgents` | `@cloudflare/agents-ui` | "Powered by Cloudflare Agents" footer badge with cloud glyph — **every example should include this** |
| `CloudflareLogo` | `@cloudflare/agents-ui` | Cloudflare cloud glyph SVG in brand orange/yellow |
| `ThemeProvider` | `@cloudflare/agents-ui/hooks` | Context provider for light/dark/system mode, persists to `localStorage` |
| `useTheme` | `@cloudflare/agents-ui/hooks` | Hook to read/set the current theme mode |
| Workers theme CSS | `@cloudflare/agents-ui/theme/workers.css` | Cloudflare-branded color overrides for Kumo tokens |
Every example's `client.tsx` should wrap the app in `<ThemeProvider>`, use `<ConnectionIndicator>` for connection state, include `<ModeToggle>` in the header, and place `<PoweredByAgents />` in the footer. Don't re-implement any of these — import from the package.
To switch back to Kumo's default theme, remove the `data-theme` attribute.
### Routing integration
Kumo's `<LinkProvider>` lets you inject a custom link component so `<Link>` renders via your router. However, there's a type mismatch between Kumo and React Router that requires an adapter.
**The problem:** Kumo's `LinkComponentProps` defines `to?: string` (optional), but React Router's `Link` requires `to: To` (non-optional, and `To = string | Partial<Path>`). These types aren't assignable in either direction — you can't pass `RouterLink` directly to `LinkProvider` without a type error.
**Our fix:** A thin `AppLink` adapter in `client.tsx` that bridges the two:
```tsx
const AppLink = forwardRef<HTMLAnchorElement, LinkComponentProps>(
({ to, ...props }, ref) => {
if (to) {
return <RouterLink ref={ref} to={to} {...props} />;
}
return <a ref={ref} {...props} />;
}
);
```
This handles the optionality gap (falls back to `<a>` when `to` is absent) and narrows `To` to `string` which is all Kumo ever passes.
**Upstream fix:** Either Kumo should make `to` required in `LinkComponentProps` (it's always provided when the component is actually called), or accept `To` from React Router. Alternatively, React Router could loosen `Link` to accept `to?: string`. Worth raising with the Kumo team since every React Router user will hit this.
## Kumo components we use
| Kumo component | Replaces |
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| `Button` | All buttons (primary, secondary, destructive, ghost actions) |
| `Input` | Text inputs; uses built-in `label` prop for Field wrapper |
| `InputArea` | Textareas |
| `Surface` | Card/panel containers |
| `Text` | Headings and body text (note: does **not** accept `className` — wrap in a `<div>` for margin/spacing) |
| `Badge` | Status indicators and tags |
| `Banner` | Alert/warning banners |
| `CodeBlock` | Static code examples and dynamic JSON display |
| `Tabs` | Tab switchers (e.g. inbox/outbox) |
| `Switch` | Boolean toggles (with built-in label) |
| `Checkbox` | Multi-select checkboxes |
| `Table` | Data tables |
| `Empty` | Empty-state placeholders |
| `Loader` | Loading spinners |
| `LinkProvider` / `Link` | Router-aware links |
## What we do custom (and why)
### Sidebar category toggle
The sidebar nav uses a raw `<button>` to expand/collapse category sections. Kumo's `Collapsible` only accepts `label: string`, but our categories include icons alongside the text label.
### Routing strategy selector (RoutingDemo)
A custom radio-like selection UI with both a title and a description per option. Kumo's `Radio.Item` only supports `label: string` — no room for the per-option description we need.
### Interactive list items
Room lists (ChatRoomsDemo), table lists (SqlDemo), email lists (ReceiveDemo, SecureDemo), and preset buttons (ApprovalDemo) use raw `<button>` elements styled as list items. These are selection-driven list rows with complex active/hover states, not standard button patterns. Kumo doesn't have a selectable list component.
### Range slider (BasicDemo)
The workflow step-count slider uses a native `<input type="range">`. Kumo doesn't ship a range/slider component.
### Log panel
The event log (`LogPanel`) uses custom CSS utility classes (`.log-entry`, `.log-entry-in`, `.log-entry-out`, `.log-entry-error`) defined in `styles.css`. These are the only custom CSS classes in the codebase. They exist because log entries are a dense, domain-specific pattern with no Kumo equivalent.
### Semantic color token gaps
A few Kumo semantic tokens we expected don't exist (yet):
- `bg-kumo-success-tint` / `bg-kumo-success` — we fall back to `bg-green-500/10` / `bg-green-500`
- `border-l-kumo-success` — we fall back to `border-l-green-500`
- These raw Tailwind greens won't adapt to `data-theme` changes (they bypass the token system)
These should be replaced with proper Kumo tokens if/when they're added upstream.