Browser Canvas
Choosing a Mode
The server auto-detects the mode based on filename:
| File | Mode | Best For |
|------|------|----------|
| App.jsx | React | Rapid prototyping, complex forms, dashboards with charts |
| index.html | Vanilla | Portable artifacts, standards-based code, durability |
React Mode (App.jsx)
- Pre-bundled shadcn/ui components (Button, Card, Dialog, etc.)
- Tailwind CSS styling
- useState/useEffect reactivity
- Recharts for data visualization
- Hot-reload preserves state
Vanilla Mode (index.html)
- Standards-based HTML/CSS/JS
- Native HTML elements (
<dialog>,<details>,<select>,<input type="date">) - Web components for composition
- CSS variables for styling
- Import maps for libraries (Chart.js, etc.)
- No build step - files served directly
- Portable - works without the server
Setup
Start the server once per session:
cd /path/to/browser-canvas && ./server.sh &
Wait for "Ready on http://localhost:9847" before proceeding.
Design First
Before creating a canvas, consider the aesthetic direction:
- Check for brand guidelines - Look for brand assets, style guides, or design tokens in the project (e.g.,
brand/,design/,.claude/canvas/) - Read
references/frontend-design.mdfor guidance on:- Typography choices (avoid generic fonts like Inter/Arial)
- Color palettes (commit to a cohesive theme)
- Motion and animations
- Spatial composition
Use the project's .claude/canvas/styles.css to implement custom fonts, colors, and effects.
Creating a Canvas
Write an App.jsx file to a new folder in .claude/artifacts/:
// Write to: .claude/artifacts/my-app/App.jsx
function App() {
return (
<Card className="w-96 mx-auto mt-8">
<CardHeader>
<CardTitle>Hello World</CardTitle>
</CardHeader>
<CardContent>
<p>This renders in the browser!</p>
</CardContent>
</Card>
);
}
The server auto-detects the new folder and opens a browser tab.
Creating a Vanilla Canvas
Write an index.html file to a new folder:
<!-- Write to: .claude/artifacts/my-app/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My App</title>
<link rel="stylesheet" href="/base.css">
<style>
/* Canvas-specific styles */
.container { padding: var(--space-6); max-width: 32rem; margin: 0 auto; }
</style>
</head>
<body>
<main class="container">
<article class="card">
<header class="card-header"><h2>Hello World</h2></header>
<div class="card-body">
<p>This renders in the browser!</p>
<button id="action">Click Me</button>
</div>
</article>
</main>
<script type="module">
document.getElementById('action').onclick = () => {
window.canvasEmit('clicked', { timestamp: Date.now() })
}
</script>
</body>
</html>
Vanilla CSS Variables (from base.css)
--color-bg, --color-fg, --color-muted, --color-primary
--color-border, --color-input, --color-danger, --color-success
--space-1 (0.25rem) through --space-8 (2rem)
--radius-sm, --radius, --radius-lg
--shadow-sm, --shadow, --shadow-lg
Vanilla Utility Classes
.card, .card-header, .card-body, .card-footer
.flex, .flex-col, .items-center, .justify-between, .gap-2, .gap-4
.text-sm, .text-muted, .font-medium, .font-bold
button, button.secondary, button.danger, button.ghost
Using Libraries in Vanilla Mode
Use import maps for external libraries:
<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'
// Use the libraries
</script>
Web Components in Vanilla Mode
Define reusable components inline:
<script>
class StatCard extends HTMLElement {
connectedCallback() {
const value = this.getAttribute('value')
const label = this.getAttribute('label')
this.innerHTML = `
<article class="card" style="padding: var(--space-4);">
<div style="font-size: var(--text-2xl); font-weight: 700; color: var(--color-primary);">${value}</div>
<div style="font-size: var(--text-sm); color: var(--color-muted);">${label}</div>
</article>
`
}
}
customElements.define('stat-card', StatCard)
</script>
<!-- Use it -->
<stat-card value="$1,234" label="Revenue"></stat-card>
Updating a Canvas
Edit the App.jsx or index.html file using the Edit tool. The browser hot-reloads automatically.
- React mode: Hot-reload preserves component state
- Vanilla mode: Full page refresh on change
Automatic Validation Feedback
When you write or edit an App.jsx file, validation errors are automatically injected into your next response. You don't need to manually check _log.jsonl for validation issues—they'll appear as context after each write.
Validation checks: ESLint (undefined variables, syntax), scope (missing components), Tailwind (invalid classes), bundle size.
Reading the Log
All canvas activity is logged to _log.jsonl. Use grep to filter by type:
# View all log entries
cat .claude/artifacts/my-app/_log.jsonl
# View recent events (user interactions)
grep '"type":"event"' .claude/artifacts/my-app/_log.jsonl | tail -10
# View errors only
grep '"severity":"error"' .claude/artifacts/my-app/_log.jsonl | head -5
# View validation notices
grep '"type":"notice"' .claude/artifacts/my-app/_log.jsonl | tail -10
# View specific issue categories
grep '"category":"scope"' .claude/artifacts/my-app/_log.jsonl # Missing components
grep '"category":"lint"' .claude/artifacts/my-app/_log.jsonl # Visual issues
grep '"category":"runtime"' .claude/artifacts/my-app/_log.jsonl # Runtime crashes
Log Entry Types
| Type | Description | Example |
|------|-------------|---------|
| event | User interactions | {"type":"event","event":"submit","data":{"name":"John"}} |
| notice | Validation issues | {"type":"notice","severity":"error","category":"scope","message":"'Foo' is not available"} |
| render | Render lifecycle | {"type":"render","status":"success","duration":42} |
| screenshot | Screenshot captures | {"type":"screenshot","path":"_screenshot.png"} |
Notice Categories
| Category | Source | Description |
|----------|--------|-------------|
| runtime | Browser | Component crashes, uncaught errors |
| lint | Browser | axe-core visual issues (contrast, etc.) |
| eslint | Server | Code quality issues |
| scope | Server | Missing components or hooks |
| tailwind | Server | Invalid Tailwind classes |
| overflow | Browser | Layout overflow detection |
| image | Browser | Broken images |
| bundle | Server | Bundle size warnings |
Severity Levels
error- Must fix (component won't render or looks broken)warning- Should fix (code smell or minor issue)info- Optional improvement
Canvas Operations (TypeScript API)
Use the CanvasClient for operations like screenshots and closing canvases:
import { CanvasClient } from "browser-canvas"
const client = await CanvasClient.fromServerJson()
// Take a screenshot
const { path } = await client.screenshot("my-app")
// Screenshot saved to: .claude/artifacts/my-app/_screenshot.png
// Close a canvas
await client.close("my-app")
// Get/set state (alternative to file-based)
const state = await client.getState("my-app")
await client.setState("my-app", { step: 2 })
// Get validation status
const status = await client.getStatus("my-app")
// { errorCount: 0, warningCount: 1, notices: [...] }
// List all canvases
const canvases = await client.list()
// Check server health
const healthy = await client.health()
Run this as a script:
bun run my-script.ts
Taking Screenshots
import { CanvasClient } from "browser-canvas"
const client = await CanvasClient.fromServerJson()
await client.screenshot("my-app")
Then read the image: .claude/artifacts/my-app/_screenshot.png
Closing a Canvas
import { CanvasClient } from "browser-canvas"
const client = await CanvasClient.fromServerJson()
await client.close("my-app")
Available Components
All components are pre-loaded and available without imports.
React Hooks
useState, useEffect, useCallback, useMemo, useRef, useReducer
useCanvasState # Two-way state sync with agent
shadcn/ui Components
Layout:
Card,CardHeader,CardTitle,CardDescription,CardContent,CardFooterDialog,DialogTrigger,DialogContent,DialogHeader,DialogTitle,DialogDescription,DialogFooterSheet,SheetTrigger,SheetContentTabs,TabsList,TabsTrigger,TabsContentAccordion,AccordionItem,AccordionTrigger,AccordionContent
Forms:
Button,Input,Textarea,LabelSelect,SelectTrigger,SelectValue,SelectContent,SelectItemCheckbox,RadioGroup,RadioGroupItemSwitch,Slider
Data Display:
Table,TableHeader,TableBody,TableRow,TableHead,TableCellBadge,Avatar,AvatarImage,AvatarFallbackProgress,Skeleton
Feedback:
Alert,AlertTitle,AlertDescriptionTooltip,TooltipTrigger,TooltipContent,TooltipProvider
Charts (Recharts)
LineChart, BarChart, PieChart, AreaChart, RadarChart
Line, Bar, Pie, Area, Radar
XAxis, YAxis, CartesianGrid, Tooltip, Legend
ResponsiveContainer, Cell
Icons (Lucide)
All lucide-react icons are available:
Check, X, Plus, Minus, ChevronRight, ChevronDown, ChevronUp, ChevronLeft,
Search, Settings, User, Mail, Phone, Calendar, Clock, Bell,
FileText, Folder, Download, Upload, Trash, Edit, Copy, Save,
Home, Menu, MoreHorizontal, MoreVertical, ExternalLink, Link,
Eye, EyeOff, Lock, Unlock, Star, Heart, ThumbsUp, ThumbsDown,
ArrowRight, ArrowLeft, ArrowUp, ArrowDown, RefreshCw, Loader2,
AlertCircle, AlertTriangle, Info, HelpCircle, CheckCircle, XCircle
Utilities
cn()- className helper (clsx + tailwind-merge)format()- date-fns format functionMarkdown- react-markdown component for rendering markdownremarkGfm- GitHub Flavored Markdown plugin (tables, strikethrough, etc.)
Emitting Events
Components send data back to Claude via window.canvasEmit():
<Button onClick={() => window.canvasEmit('clicked', { buttonId: 1 })}>
Click Me
</Button>
Events appear in _log.jsonl for Claude to read.
Event Format
{"ts":"2026-01-07T10:30:00Z","type":"event","event":"eventName","data":{"key":"value"}}
Filter events with: grep '"type":"event"' _log.jsonl | tail -10
Two-Way State Sync
For stateful artifacts that need bidirectional communication, use _state.json:
How State Works
- No state file?
useCanvasState()returns{}(empty object) - Agent writes first? Canvas sees the state immediately on load
- Canvas writes first? Creates
_state.jsonfor agent to read - Either side updates? Changes sync automatically via WebSocket
Agent Sets State
Write state to control the canvas (can be done before or after canvas loads):
// Write to: .claude/artifacts/my-wizard/_state.json
{
"step": 2,
"message": "Please confirm your details",
"formData": { "name": "John" }
}
Canvas Reads/Writes State
Use the useCanvasState hook:
function App() {
const [state, setState] = useCanvasState();
return (
<Card className="w-96 mx-auto mt-8">
<CardHeader>
<CardTitle>Step {state.step || 1}</CardTitle>
</CardHeader>
<CardContent>
<p>{state.message || "Welcome!"}</p>
<Input
value={state.formData?.name || ""}
onChange={(e) => setState({
...state,
formData: { ...state.formData, name: e.target.value }
})}
/>
</CardContent>
<CardFooter>
<Button onClick={() => setState({ ...state, confirmed: true })}>
Confirm
</Button>
</CardFooter>
</Card>
);
}
Agent Reads Updated State
# Read: .claude/artifacts/my-wizard/_state.json
{"step":2,"message":"...","formData":{"name":"John"},"confirmed":true}
State vs Events
| _state.json | _log.jsonl events |
|---------------|---------------------|
| Current snapshot | Append-only log |
| Two-way sync | One-way (canvas → agent) |
| "What's true now" | "What happened" |
| Good for: forms, wizards, settings | Good for: clicks, submissions, audit trail |
Use both together: state for current values, events for action history.
Reusable Components
Pre-built components are auto-loaded and available in your App.jsx. Use them with props for customization.
ContactForm
Validated form with customizable fields:
function App() {
return (
<ContactForm
title="Get in Touch"
description="We'll respond within 24 hours"
fields={[
{ name: "name", label: "Name", required: true },
{ name: "email", type: "email", label: "Email", required: true },
{ name: "company", label: "Company" },
{ name: "message", type: "textarea", label: "Message", required: true }
]}
submitLabel="Send Message"
onSubmit={(data) => window.canvasEmit("contact", data)}
/>
)
}
Props: title, description, fields, onSubmit, submitLabel, successMessage, showReset, className
DataChart
Flexible charting (line, bar, area, pie):
function App() {
const data = [
{ month: "Jan", sales: 4000, revenue: 2400 },
{ month: "Feb", sales: 3000, revenue: 1398 },
{ month: "Mar", sales: 2000, revenue: 9800 },
]
return (
<DataChart
type="bar"
data={data}
xKey="month"
series={[
{ dataKey: "sales", color: "#8884d8", label: "Sales" },
{ dataKey: "revenue", color: "#82ca9d", label: "Revenue" }
]}
title="Monthly Performance"
height={300}
/>
)
}
Props: type (line/bar/area/pie), data, xKey, series, title, description, height, onClick, showLegend, showGrid, formatValue
DataTable
Searchable, selectable data table:
function App() {
const users = [
{ id: 1, name: "Alice", email: "alice@example.com", status: "active" },
{ id: 2, name: "Bob", email: "bob@example.com", status: "pending" },
]
return (
<DataTable
data={users}
columns={[
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "status", label: "Status", render: (v) => <Badge>{v}</Badge> }
]}
title="Team Members"
searchable
selectable
onRowClick={(row) => window.canvasEmit("user-click", row)}
actions={[
{ icon: Pencil, onClick: (row) => window.canvasEmit("edit", row) },
{ icon: Trash2, variant: "ghost", onClick: (row) => window.canvasEmit("delete", row) }
]}
/>
)
}
Props: data, columns, title, description, searchable, selectable, onRowClick, onSelectionChange, actions, emptyMessage
StatCard
Single statistic display:
<StatCard
title="Total Revenue"
value="$45,231"
change="+20.1%"
trend="up"
icon={DollarSign}
/>
Props: title, value, change, trend (up/down), icon, onClick, description
ActivityFeed
Recent activity list:
<ActivityFeed
items={[
{ id: 1, user: "Alice", avatar: "AJ", action: "completed order #1234", time: "2 min ago" },
{ id: 2, user: "Bob", avatar: "BS", action: "signed up", time: "15 min ago" },
]}
title="Recent Activity"
onItemClick={(item) => window.canvasEmit("activity-click", item)}
/>
Props: items, title, description, onItemClick, emptyMessage, maxItems, showViewAll, onViewAll
ProgressList
Multiple progress bars:
<ProgressList
title="Goals Progress"
items={[
{ label: "Revenue Target", current: 45231, max: 50000 },
{ label: "New Customers", value: 90 },
{ label: "Orders", current: 1234, max: 1500, format: (c, m) => `${c} of ${m}` },
]}
/>
Props: items, title, description, showPercentage
MarkdownViewer
Beautiful markdown document renderer with refined typography:
const markdown = `
# Document Title
This is a paragraph with **bold** and *italic* text.
## Section
- List item one
- List item two
\`\`\`javascript
const greeting = "Hello, world!"
\`\`\`
`
function App() {
return (
<MarkdownViewer
content={markdown}
title="Documentation"
variant="default"
showTableOfContents={true}
/>
)
}
Props: content (markdown string), title, variant (default/compact/wide), showTableOfContents, className
Features: GFM tables, code blocks, blockquotes, lists, links, images, heading anchors
Custom Project Components
Create project-specific components in .claude/canvas/components/:
// .claude/canvas/components/MyWidget.jsx
/**
* MyWidget - Custom component for this project
* @prop {string} title - Widget title
*/
function MyWidget({ title }) {
return (
<Card>
<CardHeader><CardTitle>{title}</CardTitle></CardHeader>
<CardContent>Custom content here</CardContent>
</Card>
)
}
Components are auto-loaded on server start. Project components override skill components with the same name.
Project Extensions
Add custom libraries, Tailwind plugins, or CSS by creating files in .claude/canvas/.
Adding Libraries
Install packages and export them for use in canvas:
bun add react-markdown
// .claude/canvas/scope.ts
import ReactMarkdown from "react-markdown"
export const extend = {
ReactMarkdown
}
Then use in App.jsx:
function App() {
return <ReactMarkdown># Hello</ReactMarkdown>
}
Adding Tailwind Plugins
// .claude/canvas/tailwind.config.js
export default {
plugins: [
require('@tailwindcss/typography'),
],
}
Custom CSS
/* .claude/canvas/styles.css */
.custom-gradient {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.prose pre {
background: #1e1e2e;
}
Extensions are detected at server startup and bundled automatically. Restart the server after adding extensions.
Reference Documentation
Read these references as needed:
| Reference | When to Read |
|-----------|--------------|
| references/frontend-design.md | Read first - Before creating any new canvas |
| references/components.md | Need specific shadcn/ui component props or variants |
| references/charts.md | Building charts with Recharts |
| references/patterns.md | Building forms, tables, wizards, or dashboards |
Canvas Directory Structure
Artifacts are stored in .claude/artifacts/ by default (version-controllable):
.claude/artifacts/
├── server.json # Server state (port, active canvases)
├── _server.log # Server logs
├── my-form/ # React canvas
│ ├── App.jsx # React component (you write this)
│ ├── _log.jsonl # Unified log: events, notices, errors
│ ├── _state.json # Two-way state (you + canvas write)
│ └── _screenshot.png # Screenshot output (API writes)
├── data-viz/
│ └── App.jsx # Another React canvas
└── my-dashboard/ # Vanilla canvas
├── index.html # HTML/CSS/JS (you write this)
├── _log.jsonl # Same log format as React
└── _state.json # Same state sync as React
Both modes use the same file protocol for events, state, and screenshots.
Browser Toolbar
The browser displays a toolbar at the top with:
- Claude Canvas branding
- Artifact dropdown to switch between canvases
- Connection status indicator
Use the dropdown to navigate between different artifacts without leaving the browser.
Tips
- Use Tailwind classes for styling - all utilities available
- Use
useCanvasStatefor two-way communication - Emit events for actions needing audit trail
- Check
_state.jsonfor current form values - Grep
_log.jsonlfor errors:grep '"severity":"error"' _log.jsonl - Take screenshots to verify visual output
- Edit incrementally - hot-reload preserves state
Reporting Issues
Report bugs or request features:
gh issue create --repo parkerhancock/browser-canvas --title "Bug: [description]"