Canvas
A document canvas for writing with an agent: select to ask, suggested edits that stream in place, accept or reject each one, and version history.
On this page
Installation
Usage
import { Canvas } from "@/components/ui/chatcn/canvas"
import { useChatcnCanvasSuggestions } from "@/hooks/use-chatcn-canvas-suggestions"
const doc = useChatcnCanvasSuggestions(initialBlocks)
<Canvas
title="Reading width for long answers"
blocks={doc.blocks}
onBlocksChange={doc.setBlocks}
suggestions={doc.suggestions}
onAccept={doc.accept}
onReject={doc.reject}
onAcceptAll={doc.acceptAll}
onRejectAll={doc.rejectAll}
versions={doc.versions}
currentVersionId={doc.currentVersionId}
onRestoreVersion={doc.restoreVersion}
onAsk={(question, selection) => askAgent(question, selection)}
/>Stream an edit with the hook: suggest({ blockId, from, to, insert: "", status: "streaming" }) returns an id, append(id, chunk) grows the insertion and finish(id) makes it reviewable. Offsets are characters in the block's text.
The editor is plain contentEditable with title, heading and paragraph blocks: no editor dependency. Paste becomes plain text, formatting shortcuts are ignored, IME composition is native, and React never writes into the text while you type, so the caret and undo stay intact.
Props
| Prop | Type | Notes |
|---|---|---|
blocks | CanvasBlock[] | { id, type, text } with title, heading or paragraph |
onBlocksChange | (blocks) => void | Omit for read-only |
suggestions | CanvasSuggestion[] | { id, blockId, from, to, insert, status?, reason? } |
onAccept, onReject | (id) => void | |
onAcceptAll, onRejectAll | () => void | Streaming edits stay |
onStop, writingLabel | Stop while edits stream, and the status text | |
versions, currentVersionId, onRestoreVersion | The History menu | |
onAsk | (question, selection) => void | Ask field in the selection toolbar |
onSelectionAction | (action, selection) => void | Rewrite, Explain and Copy |
notice | ReactNode | A line above the document, such as agent status |
title, actions, placeholder, readOnly |
A selection arrives as { text, blockIds, start, end, range }, where start and end are { blockId, offset }.
Keyboard
| Key | Action |
|---|---|
Tab with text selected | Move into the selection toolbar |
Enter on a suggested edit | Accept it, then focus the next one |
Delete, Backspace on a suggested edit | Reject it |
ArrowDown, ArrowUp | Next and previous edit |
Enter at the end of a heading | Start a paragraph |
Cmd+Z | Native undo while editing |
Accessibility
- The editor is a multiline
textbox. While edits are pending it renders read-only, so suggestions and the text never fight over the caret. - Each edit is a group named Replace, Delete or Insert with the exact text, and a single tab stop with
aria-keyshortcuts. - Insertions are underlined and deletions struck through as well as dimmed, so the difference never depends on color.
- Streaming status is announced politely; tokens are not.
Motion
- Insertions stream in place with the caret and no per-word effects.
- An edit accepted with a pointer keeps a fill for 600 ms, then fades over 300 ms. Keyboard accepts and reduced motion skip it.
- The Accept and Reject bar moves with the active edit instantly. Hover fills take 150 ms.