@s3rd/canvas-artifacts
v0.1.6
Published
Drop-in Canvas editor for React AI applications
Downloads
57
Readme
@s3rd/canvas-artifacts
A drop-in React Canvas editor for AI applications. Give your chat UI a document editing experience — streaming-aware, fully controlled, no AI SDK lock-in.
Inspired by OpenCanvas (LangChain) and ChatGPT Canvas, distributed as a reusable npm package.
Getting Started
1. Install
npm install @s3rd/canvas-artifactsInstall peer dependencies if you don't already have them:
npm install react react-dom tailwindcss2. Import styles
Add the stylesheet once — at your app entry point or global CSS file:
import '@s3rd/canvas-artifacts/styles.css'3. Configure Tailwind
Add the package to your tailwind.config content paths so its classes are included:
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{ts,tsx}',
'./node_modules/@s3rd/canvas-artifacts/dist/**/*.{js,mjs}',
],
}4. Render the Canvas
import { Canvas } from '@s3rd/canvas-artifacts'
function App() {
const [content, setContent] = useState('# Hello\n\nStart typing...')
return <Canvas value={content} onChange={setContent} />
}value is a markdown string. onChange fires on every user edit. That's the whole API.
Demo
The demo-nextjs directory is a full working example built with Next.js and the Vercel AI SDK. It shows how to wire up a chat interface with a live canvas panel — the AI writes documents directly into the editor as it streams.
What it covers:
- Server-side tool definition with
canvasToolfrom@s3rd/canvas-artifacts/vercel-ai/server - Client-side canvas state via
useCanvasToolfrom@s3rd/canvas-artifacts/vercel-ai - Streaming document content into the editor
- Canvas action handling (polish, adjust length, suggest edits)
Run it locally:
cd demo-nextjs
cp .env.example .env.local # add your OPENAI_API_KEY
npm install
npm run devOpen http://localhost:3000 to see it in action.
Variants
inline (default)
Always renders an interactive editor in page flow.
<Canvas value={content} onChange={setContent} variant="inline" />sideview
Shows a non-interactive preview inline. Clicking Edit slides open a side panel with the full editor.
<Canvas value={content} onChange={setContent} variant="sideview" />both
Starts as an inline editor. An expand button in the toolbar transitions it to a side panel.
<Canvas value={content} onChange={setContent} variant="both" />AI streaming
The canvas handles streaming out of the box. Just update value rapidly from your AI stream — the editor locks, appends content efficiently, and unlocks when the stream settles.
onChange is not called during streaming. It only fires on user-initiated edits.
// Works with any AI SDK — Vercel AI SDK, raw fetch, anything
const [content, setContent] = useState('')
// Stream into value — canvas handles the rest
useEffect(() => {
streamFromAI((chunk) => setContent(prev => prev + chunk))
}, [])
<Canvas value={content} onChange={setContent} variant="sideview" />Panel control
Use useCanvas to control the side panel programmatically — for example, auto-opening it when an AI response finishes.
import { Canvas, useCanvas } from '@s3rd/canvas-artifacts'
function App() {
const [content, setContent] = useState('')
const canvas = useCanvas({ defaultOpen: false })
useEffect(() => {
if (streamComplete) canvas.openPanel()
}, [streamComplete])
return (
<Canvas
{...canvas}
value={content}
onChange={setContent}
variant="sideview"
/>
)
}useCanvas returns { open, onOpenChange, openPanel, closePanel, togglePanel }.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Markdown string (required) |
| onChange | (value: string) => void | — | Called on user edits (required) |
| variant | 'inline' \| 'sideview' \| 'both' | 'inline' | Layout mode |
| readOnly | boolean | false | Disables editing, shows static preview |
| toolbar | boolean \| ToolbarConfig | true | Show/hide or configure toolbar |
| open | boolean | — | Controlled panel open state |
| defaultOpen | boolean | false | Initial panel open state |
| onOpenChange | (open: boolean) => void | — | Panel open state callback |
| className | string | — | Tailwind classes on the root element |
ToolbarConfig
All buttons are enabled by default. Pass { bold: false } to hide specific buttons.
interface ToolbarConfig {
bold?: boolean
italic?: boolean
underline?: boolean
strike?: boolean
heading?: boolean // h1/h2/h3
bulletList?: boolean
orderedList?: boolean
blockquote?: boolean
code?: boolean // inline code
link?: boolean
}Theming
Tier 1 — CSS variables (no ejection needed):
:root {
--canvas-bg: #ffffff;
--canvas-border: #e2e8f0;
--canvas-toolbar-bg: #f8fafc;
--canvas-prose-color: #1a202c;
--canvas-accent: #6366f1;
}Dark mode is supported via the .dark class (shadcn/ui compatible).
Tier 2 — className prop:
<Canvas className="rounded-xl shadow-lg" value={content} onChange={setContent} />Tier 3 — Eject and own the source (shadcn-style):
npx @s3rd/canvas-artifacts add toolbar
npx @s3rd/canvas-artifacts add canvas-preview
npx @s3rd/canvas-artifacts add canvas-layoutCopies the component into src/components/canvas/ — you own it from that point. The core/ layer (editor sync, markdown bridge, state machine) stays in node_modules and receives updates automatically.
License
MIT
