Render interactive UIs in the browser using only file operations
Quick Start • Features • React • Vanilla • Docs
Browser Canvas lets Claude Code create interactive UIs by writing files. No API calls, no special protocols—just write a file and watch it render.
Two modes, same workflow:
| Mode | File | Stack |
|---|---|---|
| React | App.jsx |
shadcn/ui + Tailwind + React hooks |
| Vanilla | index.html |
Pure HTML/CSS/JS, CSS variables |
Write App.jsx or index.html → Browser opens → Edit file → Hot reload → Read _log.jsonl
# In Claude Code
/plugin install parkerhancock/browser-canvasRestart Claude Code after installation.
Requires Bun runtime.
git clone https://github.com/parkerhancock/browser-canvas.git
cd browser-canvas
bun install
./server.shThe server watches .claude/artifacts/ in your current directory. When Claude Code writes an App.jsx file there, a browser window opens automatically.
Once the server is running, Claude Code creates UIs by writing files. The server auto-detects the mode based on filename:
.claude/artifacts/my-app/App.jsx → React mode (shadcn/ui, Tailwind)
.claude/artifacts/my-app/index.html → Vanilla mode (pure HTML/CSS/JS)
Both modes share the same file protocol (_state.json, _log.jsonl) and API.
| Feature | How It Works |
|---|---|
| Dual Mode | React (App.jsx) or Vanilla (index.html) — auto-detected |
| Hot Reload | Edit files → browser updates instantly |
| Unified Log | Events, errors, validation → _log.jsonl (grep-friendly) |
| Two-Way State | Agent writes _state.json ↔ canvas reads/writes |
| TypeScript API | CanvasClient for screenshots, close, state operations |
| Validation | ESLint, scope checking, Tailwind, accessibility (axe-core) |
| Auto-Feedback | PostToolUse hook injects validation errors after writes |
| Multi-Canvas | Toolbar dropdown switches between artifacts |
Write App.jsx for rapid prototyping with pre-bundled components:
function App() {
const [count, setCount] = useState(0);
return (
<Card className="w-80 mx-auto mt-8">
<CardContent className="pt-6 text-center">
<p className="text-4xl font-bold mb-4">{count}</p>
<Button onClick={() => setCount(c => c + 1)}>+</Button>
</CardContent>
</Card>
);
}Everything is pre-bundled and available without imports:
React — useState, useEffect, useCallback, useMemo, useRef, useReducer, useCanvasState
shadcn/ui — Card, Button, Input, Table, Dialog, Tabs, Select, Checkbox, Switch, Badge, Alert, Tooltip, and more
Recharts — LineChart, BarChart, PieChart, AreaChart, ResponsiveContainer, XAxis, YAxis, Legend
Reusable Components — StatCard, DataChart, DataTable, ContactForm, ActivityFeed, ProgressList, MarkdownViewer
Utilities — cn() for classNames, format() from date-fns, Markdown, remarkGfm, Tailwind CSS
Write index.html for standards-based, portable artifacts:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My App</title>
<link rel="stylesheet" href="/base.css">
</head>
<body>
<main class="container">
<article class="card">
<header class="card-header"><h2>Counter</h2></header>
<div class="card-body">
<p class="count" id="count">0</p>
<button id="increment">+</button>
</div>
</article>
</main>
<script type="module">
let count = 0
document.getElementById('increment').onclick = () => {
count++
document.getElementById('count').textContent = count
window.canvasEmit('incremented', { count })
}
</script>
</body>
</html>- No build step — Files served directly
- CSS variables —
/base.cssprovides theming (--color-primary,--space-4, etc.) - Native elements —
<dialog>,<details>,<select>,<input type="date"> - Import maps — Load libraries from CDN without bundling
- Web components — Define custom elements for composition
- Portable — Works without the server
<script type="importmap">
{
"imports": {
"chart.js": "https://cdn.jsdelivr.net/npm/chart.js@4/auto/+esm",
"marked": "https://cdn.jsdelivr.net/npm/marked@15/+esm"
}
}
</script>
<script type="module">
import Chart from 'chart.js'
import { marked } from 'marked'
</script>Both modes support bidirectional state via _state.json.
React — Use the useCanvasState() hook:
function App() {
const [state, setState] = useCanvasState();
return (
<Button onClick={() => setState({ ...state, confirmed: true })}>
{state.message || "Confirm"}
</Button>
);
}Vanilla — Use window.canvasState():
// Read state
const state = await window.canvasState()
// Update state
await window.canvasState({ ...state, confirmed: true })Agent reads/writes _state.json:
echo '{"message":"Please confirm"}' > .claude/artifacts/my-app/_state.json
cat .claude/artifacts/my-app/_state.jsonUse CanvasClient for operations:
import { CanvasClient } from "browser-canvas"
const client = await CanvasClient.fromServerJson()
await client.screenshot("my-app") // Take screenshot
await client.close("my-app") // Close canvas
await client.getState("my-app") // Get state
await client.setState("my-app", { step: 2 }) // Set state
await client.getStatus("my-app") // Get validation status
await client.list() // List canvases
await client.health() // Check server healthAll activity goes to _log.jsonl. Use grep to filter:
grep '"type":"event"' _log.jsonl | tail -10 # User interactions
grep '"severity":"error"' _log.jsonl | head -5 # Errors only
grep '"category":"scope"' _log.jsonl # Missing componentsLog types: event, notice, render, screenshot
Notice categories: runtime, lint, eslint, scope, tailwind, overflow, image, bundle
.claude/artifacts/
├── server.json # Server state (port, active canvases)
├── _server.log # Server logs
├── react-app/ # React mode
│ ├── App.jsx # React component
│ ├── _log.jsonl # Unified log
│ ├── _state.json # Two-way state
│ └── _screenshot.png # Screenshot
└── vanilla-app/ # Vanilla mode
├── index.html # HTML/CSS/JS
├── _log.jsonl # Same log format
├── _state.json # Same state sync
└── _screenshot.png # Same screenshot API
# Default: .claude/artifacts/ in current directory
./server.sh
# Custom directory
CANVAS_DIR=/path/to/artifacts ./server.sh
./server.sh --dir /path/to/artifactsExtend browser-canvas with project-specific libraries and styles. Create files in .claude/canvas/:
| Extension | File | Purpose |
|---|---|---|
| Libraries | scope.ts |
Add npm packages to canvas scope |
| Tailwind | tailwind.config.js |
Add Tailwind plugins |
| Custom CSS | styles.css |
Custom CSS rules |
# Install a package
bun add react-markdown// .claude/canvas/scope.ts
import ReactMarkdown from "react-markdown"
export const extend = {
ReactMarkdown
}bun add -d @tailwindcss/typography// .claude/canvas/tailwind.config.js
export default {
plugins: [
require('@tailwindcss/typography'),
],
}/* .claude/canvas/styles.css */
.custom-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}Extensions are detected at server startup and bundled automatically (~50ms rebuild).
- SKILL.md — Full skill reference for Claude Code
bun run dev # Development server (with hot reload)
bun run build # Production build
bun run typecheck # Type check- Server: Bun + Hono (HTTP + WebSocket)
- Browser: react-runner with pre-bundled component scope
- Styling: Tailwind CSS + shadcn/ui
- File Watching: chokidar
- Screenshots: html2canvas
MIT
