Claude Code for Konva: 2D Canvas Graphics and Interactions — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Konva: 2D Canvas Graphics and Interactions
Frontend

Claude Code for Konva: 2D Canvas Graphics and Interactions

Published: April 29, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free