Konva is a 2D canvas library for interactive graphics — Stage is the root container with width and height. Layer groups shapes for efficient rendering. Rect, Circle, Text, Image, Line, and Arrow are built-in shape nodes. shape.on("click", handler) attaches events. shape.draggable(true) enables drag-and-drop. Transformer wraps selected shapes for resize and rotation handles. Group clusters shapes that move together. Tween animates properties over time. stage.toDataURL({ pixelRatio: 2 }) exports as PNG. react-konva wraps Konva in React with <Stage>, <Layer>, <Rect>, and <KonvaNodeComponent>. useRef<Konva.Stage>(null) accesses the imperative Konva API from React. Hit detection uses stage.getIntersection(pointer) for complex overlapping shapes. Claude Code generates Konva canvas editors, drawing tools, diagram builders, and chart overlays with transformer selection.
CLAUDE.md for Konva
## Konva Stack
- Version: konva >= 9.3, react-konva >= 18.2
- Setup: <Stage width={800} height={600}><Layer><Rect .../></Layer></Stage>
- Ref: const stageRef = useRef<Konva.Stage>(null) — access via stageRef.current
- Events: <Rect onClick={handleClick} onDragEnd={handleDragEnd} />
- Transformer: const trRef = useRef<Konva.Transformer>(null); trRef.current?.nodes([shape]) — attach to selected
- Export: stageRef.current?.toDataURL({ pixelRatio: 2, mimeType: "image/png" })
- Animate: new Konva.Tween({ node, duration: 0.5, x: 100, easing: Konva.Easings.EaseInOut }).play()
- Image: useImage(src) from "use-image" — returns [image, status]
Canvas Editor Component
// components/canvas/CanvasEditor.tsx — interactive canvas editor
"use client"
import { useRef, useState, useCallback, useEffect } from "react"
import { Stage, Layer, Rect, Circle, Text, Transformer, Group, Image as KonvaImage } from "react-konva"
import Konva from "konva"
import useImage from "use-image"
type ShapeType = "rect" | "circle" | "text"
type CanvasShape = {
id: string
type: ShapeType
x: number
y: number
width?: number
height?: number
radius?: number
text?: string
fill: string
stroke?: string
strokeWidth?: number
rotation: number
scaleX: number
scaleY: number
}
interface CanvasEditorProps {
width?: number
height?: number
onExport?: (dataUrl: string) => void
}
export function CanvasEditor({ width = 800, height = 600, onExport }: CanvasEditorProps) {
const stageRef = useRef<Konva.Stage>(null)
const transformerRef = useRef<Konva.Transformer>(null)
const [shapes, setShapes] = useState<CanvasShape[]>([
{ id: "1", type: "rect", x: 100, y: 100, width: 160, height: 80, fill: "#3B82F6", rotation: 0, scaleX: 1, scaleY: 1 },
{ id: "2", type: "circle", x: 350, y: 200, radius: 60, fill: "#10B981", rotation: 0, scaleX: 1, scaleY: 1 },
{ id: "3", type: "text", x: 150, y: 300, text: "Hello Konva", fill: "#1F2937", rotation: 0, scaleX: 1, scaleY: 1 },
])
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [activeTool, setActiveTool] = useState<ShapeType | "select">("select")
// Attach transformer to selected shapes
useEffect(() => {
const transformer = transformerRef.current
const stage = stageRef.current
if (!transformer || !stage) return
const nodes = selectedIds
.map(id => stage.findOne(`#${id}`) as Konva.Node | undefined)
.filter((n): n is Konva.Node => n !== undefined)
transformer.nodes(nodes)
transformer.getLayer()?.batchDraw()
}, [selectedIds])
const handleShapeClick = useCallback((id: string, e: Konva.KonvaEventObject<MouseEvent>) => {
if (activeTool !== "select") return
if (e.evt.shiftKey) {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id],
)
} else {
setSelectedIds([id])
}
}, [activeTool])
const handleStageClick = useCallback((e: Konva.KonvaEventObject<MouseEvent>) => {
// Click on empty stage — deselect or add shape
if (e.target === e.target.getStage()) {
if (activeTool === "select") {
setSelectedIds([])
return
}
const pos = e.target.getStage()!.getPointerPosition()!
const newShape: CanvasShape = {
id: crypto.randomUUID(),
type: activeTool,
x: pos.x,
y: pos.y,
fill: `hsl(${Math.random() * 360}, 70%, 60%)`,
rotation: 0,
scaleX: 1,
scaleY: 1,
...(activeTool === "rect" && { width: 120, height: 60 }),
...(activeTool === "circle" && { radius: 50 }),
...(activeTool === "text" && { text: "New text" }),
}
setShapes(prev => [...prev, newShape])
setActiveTool("select")
setSelectedIds([newShape.id])
}
}, [activeTool])
const handleDragEnd = useCallback((id: string, e: Konva.KonvaEventObject<DragEvent>) => {
setShapes(prev => prev.map(s =>
s.id === id ? { ...s, x: e.target.x(), y: e.target.y() } : s,
))
}, [])
const handleTransformEnd = useCallback((id: string, e: Konva.KonvaEventObject<Event>) => {
const node = e.target
setShapes(prev => prev.map(s =>
s.id === id ? {
...s,
x: node.x(),
y: node.y(),
scaleX: node.scaleX(),
scaleY: node.scaleY(),
rotation: node.rotation(),
} : s,
))
}, [])
const deleteSelected = useCallback(() => {
setShapes(prev => prev.filter(s => !selectedIds.includes(s.id)))
setSelectedIds([])
}, [selectedIds])
const exportCanvas = useCallback(() => {
const dataUrl = stageRef.current?.toDataURL({ pixelRatio: 2, mimeType: "image/png" })
if (dataUrl) onExport?.(dataUrl)
}, [onExport])
return (
<div className="space-y-3">
{/* Toolbar */}
<div className="flex items-center gap-2 p-2 bg-muted rounded-lg">
{(["select", "rect", "circle", "text"] as const).map(tool => (
<button
key={tool}
onClick={() => setActiveTool(tool)}
className={`px-3 py-1.5 rounded text-sm capitalize ${activeTool === tool ? "bg-primary text-primary-foreground" : "hover:bg-background"}`}
>
{tool === "select" ? "Select" : tool === "rect" ? "Rectangle" : tool === "circle" ? "Circle" : "Text"}
</button>
))}
<div className="ml-auto flex gap-2">
{selectedIds.length > 0 && (
<button onClick={deleteSelected} className="btn-danger text-sm px-3 py-1.5">
Delete
</button>
)}
<button onClick={exportCanvas} className="btn-outline text-sm px-3 py-1.5">
Export PNG
</button>
</div>
</div>
{/* Canvas */}
<div className="rounded-xl border overflow-hidden" style={{ width, height }}>
<Stage
ref={stageRef}
width={width}
height={height}
onClick={handleStageClick}
style={{ background: "#fff" }}
>
<Layer>
{shapes.map(shape => {
const isSelected = selectedIds.includes(shape.id)
const commonProps = {
id: shape.id,
x: shape.x,
y: shape.y,
rotation: shape.rotation,
scaleX: shape.scaleX,
scaleY: shape.scaleY,
draggable: activeTool === "select",
onClick: (e: Konva.KonvaEventObject<MouseEvent>) => handleShapeClick(shape.id, e),
onDragEnd: (e: Konva.KonvaEventObject<DragEvent>) => handleDragEnd(shape.id, e),
onTransformEnd: (e: Konva.KonvaEventObject<Event>) => handleTransformEnd(shape.id, e),
}
if (shape.type === "rect") {
return <Rect key={shape.id} {...commonProps} width={shape.width!} height={shape.height!} fill={shape.fill} strokeWidth={isSelected ? 2 : 0} stroke={isSelected ? "#3B82F6" : undefined} cornerRadius={4} />
}
if (shape.type === "circle") {
return <Circle key={shape.id} {...commonProps} radius={shape.radius!} fill={shape.fill} />
}
if (shape.type === "text") {
return <Text key={shape.id} {...commonProps} text={shape.text!} fill={shape.fill} fontSize={18} fontFamily="Inter, sans-serif" />
}
return null
})}
<Transformer
ref={transformerRef}
boundBoxFunc={(oldBox, newBox) => {
// Minimum size
if (newBox.width < 20 || newBox.height < 20) return oldBox
return newBox
}}
/>
</Layer>
</Stage>
</div>
<p className="text-xs text-muted-foreground">
Click stage to add shapes • Shift-click to multi-select • Drag handles to resize
</p>
</div>
)
}
For the Fabric.js alternative when a more feature-rich canvas library with built-in object serialization to/from JSON, free drawing mode, text editing, and SVG imports is preferred — Fabric.js has a wider feature set than Konva for document-editor use cases, though Konva has better performance for games and animation, see the Fabric.js guide. For the React Three Fiber alternative when 3D canvas rendering with WebGL, 3D geometry, lighting, and physics is needed instead of 2D canvas — Three.js via react-three-fiber handles the third dimension with a declarative React API, see the React Three Fiber guide. The Claude Skills 360 bundle includes Konva skill sets covering canvas editors, drag interactions, and export. Start with the free tier to try canvas application generation.