Render interactive React UIs in a browser window using file operations. Use when users ask to: show forms, render charts/graphs, create dashboards, display data tables, build visual interfaces, or show any UI component. Trigger phrases: "show me", "render", "display", "create a form/chart/table/dashboard".
/plugin marketplace add parkerhancock/browser-canvas/plugin install browser-canvas@browser-canvas-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
components/ActivityFeed.jsxcomponents/ContactForm.jsxcomponents/DataChart.jsxcomponents/DataTable.jsxcomponents/MarkdownViewer.jsxcomponents/ProgressList.jsxcomponents/StatCard.jsxreferences/charts.mdreferences/components.mdreferences/frontend-design.mdreferences/patterns.mdStart the server once per session:
cd /path/to/browser-canvas && ./server.sh &
Wait for "Ready on http://localhost:9847" before proceeding.
Before creating a canvas, consider the aesthetic direction:
brand/, design/, .claude/canvas/)references/frontend-design.md for guidance on:
Use the project's .claude/canvas/styles.css to implement custom fonts, colors, and effects.
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.
Edit the App.jsx file using the Edit tool. The browser hot-reloads automatically.
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.
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
| 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"} |
| 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 |
error - Must fix (component won't render or looks broken)warning - Should fix (code smell or minor issue)info - Optional improvementUse 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
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
import { CanvasClient } from "browser-canvas"
const client = await CanvasClient.fromServerJson()
await client.close("my-app")
All components are pre-loaded and available without imports.
useState, useEffect, useCallback, useMemo, useRef, useReducer
useCanvasState # Two-way state sync with agent
Layout:
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooterDialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooterSheet, SheetTrigger, SheetContentTabs, TabsList, TabsTrigger, TabsContentAccordion, AccordionItem, AccordionTrigger, AccordionContentForms:
Button, Input, Textarea, LabelSelect, SelectTrigger, SelectValue, SelectContent, SelectItemCheckbox, RadioGroup, RadioGroupItemSwitch, SliderData Display:
Table, TableHeader, TableBody, TableRow, TableHead, TableCellBadge, Avatar, AvatarImage, AvatarFallbackProgress, SkeletonFeedback:
Alert, AlertTitle, AlertDescriptionTooltip, TooltipTrigger, TooltipContent, TooltipProviderLineChart, BarChart, PieChart, AreaChart, RadarChart
Line, Bar, Pie, Area, Radar
XAxis, YAxis, CartesianGrid, Tooltip, Legend
ResponsiveContainer, Cell
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
cn() - className helper (clsx + tailwind-merge)format() - date-fns format functionMarkdown - react-markdown component for rendering markdownremarkGfm - GitHub Flavored Markdown plugin (tables, strikethrough, etc.)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.
{"ts":"2026-01-07T10:30:00Z","type":"event","event":"eventName","data":{"key":"value"}}
Filter events with: grep '"type":"event"' _log.jsonl | tail -10
For stateful artifacts that need bidirectional communication, use _state.json:
useCanvasState() returns {} (empty object)_state.json for agent to readWrite 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" }
}
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>
);
}
# Read: .claude/artifacts/my-wizard/_state.json
{"step":2,"message":"...","formData":{"name":"John"},"confirmed":true}
_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.
Pre-built components are auto-loaded and available in your App.jsx. Use them with props for customization.
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
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
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
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
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
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
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
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.
Add custom libraries, Tailwind plugins, or CSS by creating files in .claude/canvas/.
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>
}
// .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%);
}
.prose pre {
background: #1e1e2e;
}
Extensions are detected at server startup and bundled automatically. Restart the server after adding extensions.
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 |
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/ # Canvas folder
│ ├── App.jsx # Component code (you write this)
│ ├── _log.jsonl # Unified log: events, notices, errors (server writes)
│ ├── _state.json # Two-way state (you + canvas write)
│ └── _screenshot.png # Screenshot output (API writes)
└── data-viz/
└── App.jsx
The browser displays a toolbar at the top with:
Use the dropdown to navigate between different artifacts without leaving the browser.
useCanvasState for two-way communication_state.json for current form values_log.jsonl for errors: grep '"severity":"error"' _log.jsonlReport bugs or request features:
gh issue create --repo parkerhancock/browser-canvas --title "Bug: [description]"