Claude Code for Fabric.js: Feature-Rich Canvas Editing — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Fabric.js: Feature-Rich Canvas Editing
Frontend

Claude Code for Fabric.js: Feature-Rich Canvas Editing

Published: May 5, 2027
Read time: 7 min read
By: Claude Skills 360

Fabric.js is an interactive canvas library with built-in serialization and drawing — new fabric.Canvas("id", { width, height }) creates the canvas. fabric.Rect, fabric.Circle, fabric.Path, and fabric.Image.fromURL are shape objects. canvas.add(obj) adds objects; canvas.remove(obj) removes them. canvas.isDrawingMode = true activates freehand drawing with canvas.freeDrawingBrush. shape.toJSON() / canvas.loadFromJSON(json) serialize the canvas state for save/load. fabric.Group clusters objects into one selectable unit. fabric.loadSVGFromString(svgString, callback) imports arbitrary SVG. image.filters.push(new fabric.Image.filters.Grayscale()) + image.applyFilters() adds visual effects. canvas.toDataURL({ format: "png", multiplier: 2 }) exports full canvas. canvas.on("object:selected") and canvas.on("object:modified") track selection state. Claude Code generates Fabric.js canvas editors, signature pads, banner designers, photo editors, and diagram tools.

CLAUDE.md for Fabric.js

## Fabric.js Stack
- Version: fabric >= 6.3, @types/fabric >= 5.3
- Init: const canvas = new fabric.Canvas("canvas-id", { width: 800, height: 600, selection: true })
- Add: canvas.add(new fabric.Rect({ left: 100, top: 100, width: 150, height: 80, fill: "#3B82F6" }))
- Events: canvas.on("object:modified", ({ target }) => pushHistory(canvas.toJSON()))
- Drawing: canvas.isDrawingMode = true; canvas.freeDrawingBrush.width = 4; canvas.freeDrawingBrush.color = "#000"
- Serialize: const json = canvas.toJSON(["id", "data"]); canvas.loadFromJSON(json, canvas.renderAll.bind(canvas))
- Export: canvas.toDataURL({ format: "png", multiplier: 2 })
- Image: fabric.Image.fromURL(url, (img) => { img.scale(0.5); canvas.add(img) })

Canvas Editor Setup

// components/canvas/FabricEditor.tsx — full canvas editor
"use client"
import { useEffect, useRef, useState, useCallback } from "react"
import { fabric } from "fabric"

type Tool = "select" | "draw" | "rect" | "circle" | "text" | "image"

interface EditorState {
  activeTool: Tool
  strokeColor: string
  fillColor: string
  strokeWidth: number
  fontSize: number
  canUndo: boolean
  canRedo: boolean
}

const MAX_HISTORY = 50

export function FabricEditor() {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const fabricRef = useRef<fabric.Canvas | null>(null)
  const historyRef = useRef<string[]>([])
  const historyIndexRef = useRef(-1)
  const isHistoryActionRef = useRef(false)

  const [state, setState] = useState<EditorState>({
    activeTool: "select",
    strokeColor: "#1F2937",
    fillColor: "#3B82F6",
    strokeWidth: 2,
    fontSize: 20,
    canUndo: false,
    canRedo: false,
  })

  // Initialize Fabric.js canvas
  useEffect(() => {
    if (!canvasRef.current) return

    const canvas = new fabric.Canvas(canvasRef.current, {
      width: 900,
      height: 600,
      backgroundColor: "#ffffff",
      preserveObjectStacking: true,
    })
    fabricRef.current = canvas

    // Push initial state
    pushHistory(canvas)

    // History tracking
    const onModified = () => {
      if (!isHistoryActionRef.current) pushHistory(canvas)
    }

    canvas.on("object:added", onModified)
    canvas.on("object:modified", onModified)
    canvas.on("object:removed", onModified)

    return () => {
      canvas.dispose()
      fabricRef.current = null
    }
  }, [])

  const pushHistory = useCallback((canvas: fabric.Canvas) => {
    const json = JSON.stringify(canvas.toJSON(["id", "lockMovementX", "lockMovementY"]))
    const history = historyRef.current
    const index = historyIndexRef.current

    // Truncate redo history on new action
    historyRef.current = history.slice(0, index + 1)
    historyRef.current.push(json)

    // Cap history
    if (historyRef.current.length > MAX_HISTORY) {
      historyRef.current = historyRef.current.slice(-MAX_HISTORY)
    }

    historyIndexRef.current = historyRef.current.length - 1
    setState(prev => ({
      ...prev,
      canUndo: historyIndexRef.current > 0,
      canRedo: false,
    }))
  }, [])

  const undo = useCallback(() => {
    if (historyIndexRef.current <= 0) return
    historyIndexRef.current--
    restoreHistory()
  }, [])

  const redo = useCallback(() => {
    if (historyIndexRef.current >= historyRef.current.length - 1) return
    historyIndexRef.current++
    restoreHistory()
  }, [])

  const restoreHistory = useCallback(() => {
    const canvas = fabricRef.current
    if (!canvas) return

    isHistoryActionRef.current = true
    const json = JSON.parse(historyRef.current[historyIndexRef.current])

    canvas.loadFromJSON(json, () => {
      canvas.renderAll()
      isHistoryActionRef.current = false
      setState(prev => ({
        ...prev,
        canUndo: historyIndexRef.current > 0,
        canRedo: historyIndexRef.current < historyRef.current.length - 1,
      }))
    })
  }, [])

  const setTool = useCallback((tool: Tool) => {
    const canvas = fabricRef.current
    if (!canvas) return

    canvas.isDrawingMode = tool === "draw"
    canvas.selection = tool === "select"

    setState(prev => ({ ...prev, activeTool: tool }))

    if (tool === "draw") {
      canvas.freeDrawingBrush.width = state.strokeWidth
      canvas.freeDrawingBrush.color = state.strokeColor
    }
  }, [state.strokeColor, state.strokeWidth])

  const addShape = useCallback((type: "rect" | "circle") => {
    const canvas = fabricRef.current
    if (!canvas) return

    const center = canvas.getCenter()
    const common = {
      left: center.left - 60,
      top: center.top - 40,
      fill: state.fillColor,
      stroke: state.strokeColor,
      strokeWidth: state.strokeWidth,
    }

    const shape = type === "rect"
      ? new fabric.Rect({ ...common, width: 120, height: 80, rx: 6, ry: 6 })
      : new fabric.Circle({ ...common, radius: 60 })

    canvas.add(shape)
    canvas.setActiveObject(shape)
    canvas.renderAll()
  }, [state.fillColor, state.strokeColor, state.strokeWidth])

  const addText = useCallback(() => {
    const canvas = fabricRef.current
    if (!canvas) return

    const center = canvas.getCenter()
    const text = new fabric.IText("Click to edit", {
      left: center.left - 80,
      top: center.top - 12,
      fontSize: state.fontSize,
      fill: state.strokeColor,
      fontFamily: "Inter, sans-serif",
    })
    canvas.add(text)
    canvas.setActiveObject(text)
    text.enterEditing()
  }, [state.fontSize, state.strokeColor])

  const deleteSelected = useCallback(() => {
    const canvas = fabricRef.current
    if (!canvas) return
    const active = canvas.getActiveObjects()
    canvas.remove(...active)
    canvas.discardActiveObject()
    canvas.renderAll()
  }, [])

  const exportPNG = useCallback(() => {
    const canvas = fabricRef.current
    if (!canvas) return
    const dataUrl = canvas.toDataURL({ format: "png", multiplier: 2 })
    const a = document.createElement("a")
    a.href = dataUrl
    a.download = "canvas.png"
    a.click()
  }, [])

  const loadImage = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    const url = URL.createObjectURL(file)

    fabric.Image.fromURL(url, (img) => {
      const canvas = fabricRef.current
      if (!canvas) return

      // Scale to fit within canvas
      const scale = Math.min(
        (canvas.getWidth() * 0.5) / (img.width ?? 1),
        (canvas.getHeight() * 0.5) / (img.height ?? 1),
        1,
      )
      img.scale(scale)
      img.set({ left: 50, top: 50 })
      canvas.add(img)
      canvas.setActiveObject(img)
      canvas.renderAll()
      URL.revokeObjectURL(url)
    })
  }, [])

  return (
    <div className="space-y-3">
      {/* Toolbar */}
      <div className="flex items-center gap-2 p-2 bg-muted rounded-xl flex-wrap">
        {(["select", "draw", "rect", "circle", "text"] as const).map(tool => (
          <button
            key={tool}
            onClick={() => tool === "rect" || tool === "circle" ? addShape(tool) : tool === "text" ? addText() : setTool(tool)}
            className={`px-3 py-1.5 rounded text-sm capitalize ${state.activeTool === tool ? "bg-primary text-primary-foreground" : "hover:bg-background"}`}
          >
            {tool}
          </button>
        ))}

        <div className="flex items-center gap-1 ml-2">
          <label className="flex items-center gap-1 text-xs text-muted-foreground">
            Fill
            <input
              type="color"
              value={state.fillColor}
              onChange={e => setState(prev => ({ ...prev, fillColor: e.target.value }))}
              className="h-7 w-10 cursor-pointer rounded border"
            />
          </label>
          <label className="flex items-center gap-1 text-xs text-muted-foreground">
            Stroke
            <input
              type="color"
              value={state.strokeColor}
              onChange={e => setState(prev => ({ ...prev, strokeColor: e.target.value }))}
              className="h-7 w-10 cursor-pointer rounded border"
            />
          </label>
        </div>

        <div className="ml-auto flex gap-2">
          <button onClick={undo} disabled={!state.canUndo} className="btn-ghost text-sm px-2 py-1 disabled:opacity-40">↩ Undo</button>
          <button onClick={redo} disabled={!state.canRedo} className="btn-ghost text-sm px-2 py-1 disabled:opacity-40">↪ Redo</button>
          <button onClick={deleteSelected} className="btn-ghost text-sm px-2 py-1 text-red-500">Delete</button>
          <label className="btn-outline text-sm px-3 py-1.5 cursor-pointer">
            Image
            <input type="file" accept="image/*" className="hidden" onChange={loadImage} />
          </label>
          <button onClick={exportPNG} className="btn-primary text-sm px-3 py-1.5">Export PNG</button>
        </div>
      </div>

      {/* Canvas */}
      <div className="rounded-xl border overflow-hidden">
        <canvas ref={canvasRef} />
      </div>
    </div>
  )
}

Signature Pad

// components/canvas/SignaturePad.tsx — signature capture
"use client"
import { useEffect, useRef, useCallback, useState } from "react"
import { fabric } from "fabric"

interface SignaturePadProps {
  width?: number
  height?: number
  onSave?: (dataUrl: string) => void
}

export function SignaturePad({ width = 500, height = 200, onSave }: SignaturePadProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const fabricRef = useRef<fabric.Canvas | null>(null)
  const [hasSignature, setHasSignature] = useState(false)

  useEffect(() => {
    if (!canvasRef.current) return

    const canvas = new fabric.Canvas(canvasRef.current, {
      width,
      height,
      backgroundColor: "#ffffff",
      isDrawingMode: true,
    })

    canvas.freeDrawingBrush.color = "#1F2937"
    canvas.freeDrawingBrush.width = 2

    canvas.on("path:created", () => setHasSignature(true))

    fabricRef.current = canvas
    return () => { canvas.dispose() }
  }, [width, height])

  const clear = useCallback(() => {
    fabricRef.current?.clear()
    fabricRef.current?.setBackgroundColor("#ffffff", () => fabricRef.current?.renderAll())
    setHasSignature(false)
  }, [])

  const save = useCallback(() => {
    const canvas = fabricRef.current
    if (!canvas || !hasSignature) return
    const dataUrl = canvas.toDataURL({ format: "png", multiplier: 1 })
    onSave?.(dataUrl)
  }, [hasSignature, onSave])

  return (
    <div className="space-y-2">
      <div className="border-2 border-dashed rounded-xl overflow-hidden" style={{ width, height }}>
        <canvas ref={canvasRef} />
      </div>
      <div className="flex gap-2">
        <button onClick={clear} className="btn-ghost text-sm px-3 py-1.5">Clear</button>
        <button onClick={save} disabled={!hasSignature} className="btn-primary text-sm px-3 py-1.5 disabled:opacity-50">Save Signature</button>
      </div>
    </div>
  )
}

For the Konva/react-konva alternative when React-idiomatic canvas with a declarative JSX API, better TypeScript types, and simpler state synchronization in React apps is preferred — Konva directly maps Canvas nodes to React components, while Fabric.js has a more imperative API better suited to complex editing tools with JSON serialization, see the Konva guide. For the Excalidraw alternative when a fully featured, open-source collaborative whiteboard with hand-drawn style and built-in shapes is needed — Excalidraw embeds as a React component with no setup while Fabric.js requires more custom implementation, see the Excalidraw guide. The Claude Skills 360 bundle includes Fabric.js skill sets covering canvas editors, signature pads, and image filters. Start with the free tier to try canvas drawing 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