Agent Skills: Browser Canvas

|

UncategorizedID: parkerhancock/browser-canvas/canvas

Skill Files

Browse the full folder contents for canvas.

Download Skill

Loading file tree…

skills/canvas/SKILL.md

Skill Metadata

Name
canvas
Description
|

Browser Canvas

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:

  1. Check for brand guidelines - Look for brand assets, style guides, or design tokens in the project (e.g., brand/, design/, .claude/canvas/)
  2. Read references/frontend-design.md for 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.

Updating a Canvas

Edit the App.jsx file using the Edit tool. The browser hot-reloads automatically.

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, CardFooter
  • Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
  • Sheet, SheetTrigger, SheetContent
  • Tabs, TabsList, TabsTrigger, TabsContent
  • Accordion, AccordionItem, AccordionTrigger, AccordionContent

Forms:

  • Button, Input, Textarea, Label
  • Select, SelectTrigger, SelectValue, SelectContent, SelectItem
  • Checkbox, RadioGroup, RadioGroupItem
  • Switch, Slider

Data Display:

  • Table, TableHeader, TableBody, TableRow, TableHead, TableCell
  • Badge, Avatar, AvatarImage, AvatarFallback
  • Progress, Skeleton

Feedback:

  • Alert, AlertTitle, AlertDescription
  • Tooltip, 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 function
  • Markdown - react-markdown component for rendering markdown
  • remarkGfm - 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.json for 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/                    # 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

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

  1. Use Tailwind classes for styling - all utilities available
  2. Use useCanvasState for two-way communication
  3. Emit events for actions needing audit trail
  4. Check _state.json for current form values
  5. Grep _log.jsonl for errors: grep '"severity":"error"' _log.jsonl
  6. Take screenshots to verify visual output
  7. Edit incrementally - hot-reload preserves state

Reporting Issues

Report bugs or request features:

gh issue create --repo parkerhancock/browser-canvas --title "Bug: [description]"