fabric-editor-kit
v0.1.1
Published
Professional production-grade React editor toolkit for Fabric.js — contextual toolbars, layer panel, export pipeline, context menu and dynamic theming.
Maintainers
Readme
fabric-editor-kit
A professional, production-grade React wrapper for Fabric.js.
Drop-in contextual toolbars, layer panel, export pipeline, right-click context menu, and dynamic theming — everything a Canva-style editor needs, out of the box.
Getting Started · Components · Theming · Hooks · Roadmap
Why fabric-editor-kit?
Raw Fabric.js gives you a powerful canvas engine — but zero UI. Building a Canva-style editor means hand-crafting toolbars for every object type, managing undo/redo, writing export logic, and wiring up a layer panel. That is weeks of work before you write a single line of your actual product.
fabric-editor-kit solves all of that.
| Without fabric-editor-kit | With fabric-editor-kit |
|---|---|
| Build toolbar for every object type | Drop in <TextToolbar />, <ShapeToolbar /> etc. |
| Write undo/redo from scratch | Built into FabricKitProvider |
| Custom context menu | useContextMenu() hook, ready to wire |
| No layer management UI | <LayerPanel /> with drag reorder |
| Manual PNG/SVG/JSON export | <ExportPanel /> or useExport() hook |
| Hard-coded colors, no theming | createTheme() with full token system |
Getting Started
Installation
npm install fabric-editor-kit fabric
# or
yarn add fabric-editor-kit fabricPeer requirements: React 18+, Fabric.js 6.x, TypeScript 5.x (optional but recommended)
Minimal Setup
TSX
import { useRef } from 'react'
import { FabricKitProvider, ThemeProvider, createTheme } from 'fabric-editor-kit'
export default function App() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const theme = createTheme('dark') // or 'light'
return (
<ThemeProvider theme={theme}>
<FabricKitProvider canvasRef={canvasRef} width={800} height={500}>
{/* Your editor UI here */}
<canvas ref={canvasRef} />
</FabricKitProvider>
</ThemeProvider>
)
}JSX
import { useRef } from 'react'
import { FabricKitProvider, ThemeProvider, createTheme } from 'fabric-editor-kit'
export default function App() {
const canvasRef = useRef(null)
const theme = createTheme('dark') // or 'light'
return (
<ThemeProvider theme={theme}>
<FabricKitProvider canvasRef={canvasRef} width={800} height={500}>
{/* Your editor UI here */}
<canvas ref={canvasRef} />
</FabricKitProvider>
</ThemeProvider>
)
}Full Editor Setup
TSX
import { useRef, useEffect } from 'react'
import type { RefObject } from 'react'
import { Rect, Circle, Textbox } from 'fabric'
import {
FabricKitProvider, useFabricKit,
ThemeProvider, createTheme,
CanvasToolbar, TextToolbar, ShapeToolbar,
ImageToolbar, MultiToolbar,
ContextMenu, useContextMenu,
LayerPanel, ExportPanel,
} from 'fabric-editor-kit'
function EditorInner({ canvasRef }: { canvasRef: RefObject<HTMLCanvasElement | null> }) {
const { canvas, selectionType, undo, redo, canUndo, canRedo } = useFabricKit()
useEffect(() => {
if (!canvas) return
canvas.add(
new Rect({ left: 60, top: 60, width: 160, height: 100, fill: '#6366f1', rx: 8 }),
new Circle({ left: 280, top: 60, radius: 55, fill: '#0ea5e9' }),
new Textbox('Double-click to edit', { left: 60, top: 220, fontSize: 20, fill: '#1e293b', width: 240 }),
)
canvas.renderAll()
}, [canvas])
const { menu, open, close } = useContextMenu({
canvas, onUndo: undo, onRedo: redo, canUndo, canRedo,
})
useEffect(() => {
if (!canvas) return
const el = canvas.getElement().parentElement
if (!el) return
el.addEventListener('contextmenu', open)
return () => el.removeEventListener('contextmenu', open)
}, [canvas, open])
return (
<div style={{
minHeight: '100vh',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f1f5f9',
padding: '32px',
boxSizing: 'border-box',
}}>
{menu && <ContextMenu x={menu.x} y={menu.y} items={menu.items} onClose={close} />}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'center' }}>
{/* Toolbar — centered above canvas */}
<div style={{ display: 'flex', justifyContent: 'center' }}>
{selectionType === 'none' && <CanvasToolbar />}
{selectionType === 'text' && <TextToolbar />}
{selectionType === 'shape' && <ShapeToolbar />}
{selectionType === 'image' && <ImageToolbar />}
{selectionType === 'group' && <MultiToolbar />}
{selectionType === 'multi' && <MultiToolbar />}
</div>
{/* Canvas + side panels row */}
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
<div style={{
borderRadius: '12px',
overflow: 'hidden',
border: '1.5px solid #cbd5e1',
boxShadow: '0 4px 24px 0 rgba(0,0,0,0.10), 0 1.5px 6px 0 rgba(0,0,0,0.07)',
background: '#ffffff',
lineHeight: 0,
}}>
<canvas ref={canvasRef} style={{ display: 'block' }} />
</div>
{/* Side panels */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<LayerPanel width={220} />
<ExportPanel width={220} filename="my-design" />
</div>
</div>
</div>
</div>
)
}
export default function App() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const theme = createTheme('dark')
return (
<ThemeProvider theme={theme}>
<FabricKitProvider canvasRef={canvasRef} width={800} height={500} backgroundColor="#ffffff">
<EditorInner canvasRef={canvasRef} />
</FabricKitProvider>
</ThemeProvider>
)
}JSX
import { useRef, useEffect } from 'react'
import { Rect, Circle, Textbox } from 'fabric'
import {
FabricKitProvider, useFabricKit,
ThemeProvider, createTheme,
CanvasToolbar, TextToolbar, ShapeToolbar,
ImageToolbar, MultiToolbar,
ContextMenu, useContextMenu,
LayerPanel, ExportPanel,
} from 'fabric-editor-kit'
function EditorInner({ canvasRef }) {
const { canvas, selectionType, undo, redo, canUndo, canRedo } = useFabricKit()
useEffect(() => {
if (!canvas) return
canvas.add(
new Rect({ left: 60, top: 60, width: 160, height: 100, fill: '#6366f1', rx: 8 }),
new Circle({ left: 280, top: 60, radius: 55, fill: '#0ea5e9' }),
new Textbox('Double-click to edit', { left: 60, top: 220, fontSize: 20, fill: '#1e293b', width: 240 }),
)
canvas.renderAll()
}, [canvas])
const { menu, open, close } = useContextMenu({
canvas, onUndo: undo, onRedo: redo, canUndo, canRedo,
})
useEffect(() => {
if (!canvas) return
const el = canvas.getElement().parentElement
if (!el) return
el.addEventListener('contextmenu', open)
return () => el.removeEventListener('contextmenu', open)
}, [canvas, open])
return (
<div style={{
minHeight: '100vh',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f1f5f9',
padding: '32px',
boxSizing: 'border-box',
}}>
{menu && <ContextMenu x={menu.x} y={menu.y} items={menu.items} onClose={close} />}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'center' }}>
{/* Toolbar — centered above canvas */}
<div style={{ display: 'flex', justifyContent: 'center' }}>
{selectionType === 'none' && <CanvasToolbar />}
{selectionType === 'text' && <TextToolbar />}
{selectionType === 'shape' && <ShapeToolbar />}
{selectionType === 'image' && <ImageToolbar />}
{selectionType === 'group' && <MultiToolbar />}
{selectionType === 'multi' && <MultiToolbar />}
</div>
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start' }}>
<div style={{
borderRadius: '12px',
overflow: 'hidden',
border: '1.5px solid #cbd5e1',
boxShadow: '0 4px 24px 0 rgba(0,0,0,0.10), 0 1.5px 6px 0 rgba(0,0,0,0.07)',
background: '#ffffff',
lineHeight: 0,
}}>
<canvas ref={canvasRef} style={{ display: 'block' }} />
</div>
{/* Side panels */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<LayerPanel width={220} />
<ExportPanel width={220} filename="my-design" />
</div>
</div>
</div>
</div>
)
}
export default function App() {
const canvasRef = useRef(null)
const theme = createTheme('dark')
return (
<ThemeProvider theme={theme}>
<FabricKitProvider canvasRef={canvasRef} width={800} height={500} backgroundColor="#ffffff">
<EditorInner canvasRef={canvasRef} />
</FabricKitProvider>
</ThemeProvider>
)
}Components
<FabricKitProvider />
The root provider. Initializes the Fabric.js canvas and exposes context to all child components.
<FabricKitProvider
canvasRef={canvasRef} // RefObject<HTMLCanvasElement>
width={800} // Canvas width in px
height={500} // Canvas height in px
backgroundColor="#fff" // Optional canvas background
>
{children}
</FabricKitProvider>Toolbars
All toolbars auto-read from canvas selection state via useFabricKit(). No props required.
<CanvasToolbar />
Shows when nothing is selected. Controls: zoom in/out/reset, undo/redo, clear canvas, background color.
{selectionType === 'none' && <CanvasToolbar />}<TextToolbar />
Shows when a text/textbox object is selected. Controls: font family, font size, bold, italic, underline, strikethrough, alignment, color, letter spacing, line height, text case.
{selectionType === 'text' && <TextToolbar />}
// Add to existing fonts
{selectionType === 'text' &&
<TextToolbar
replaceDefaultFonts // Replace all defaults with only your fonts
fonts={[
{ label: 'Ubuntu', value: 'Ubuntu' },
]}
/>
}<ShapeToolbar />
Shows when a rect/circle/polygon/path is selected. Controls: fill color, stroke color, stroke width, border radius, opacity, flip H/V, bring to front/send to back.
{selectionType === 'shape' && <ShapeToolbar />}<ImageToolbar />
Shows when an image object is selected. Controls: brightness, contrast, saturation, blur, grayscale toggle, sepia toggle, opacity, flip H/V, replace image.
{selectionType === 'image' && <ImageToolbar />}<MultiToolbar />
Shows when multiple objects are drag-selected, or when a group is selected. Controls: align (left/center/right/top/middle/bottom), distribute (horizontal/vertical), group/ungroup, flip H/V, opacity, bring to front/send to back.
{selectionType === 'multi' && <MultiToolbar />}
{selectionType === 'group' && <MultiToolbar />}<LayerPanel />
Displays all canvas objects as a stacked layer list with full management controls.
<LayerPanel
width={220} // Panel width in px (default: 220)
/>Features:
- Click row → selects object on canvas
- Drag row → reorders z-index
- 👁 Eye icon → toggle visibility
- 🔒 Lock icon → toggle selectability
- 🗑 Delete icon → removes from canvas
- Double-click name → inline rename
<ExportPanel />
Export or import the canvas with a format selector and quality controls.
<ExportPanel
width={220} // Panel width in px (default: 220)
filename="my-design" // Base filename without extension (default: 'canvas-export')
/>Supported formats: | Format | Notes | |---|---| | PNG | Lossless, supports transparency. Scale 1×/2×/3× | | JPEG | Lossy, smaller files. Quality slider 10–100% | | SVG | Full vector. Images embedded as base64 | | JSON | Complete canvas state. Re-importable |
<ContextMenu />
Renders a floating context menu at given coordinates.
{menu && (
<ContextMenu
x={menu.x}
y={menu.y}
items={menu.items}
onClose={close}
/>
)}Hooks
useFabricKit()
Access canvas instance and selection state anywhere inside FabricKitProvider.
const {
canvas, // Fabric.Canvas instance
activeObject, // Currently selected FabricObject | null
selectedObjects, // Array of selected objects (multi-select)
selectionType, // 'none' | 'text' | 'shape' | 'image' | 'group' | 'multi'
undo, // () => void
redo, // () => void
canUndo, // boolean
canRedo, // boolean
} = useFabricKit()Example — add a rectangle programmatically:
import { Rect } from 'fabric'
import { useFabricKit } from 'fabric-editor-kit'
function AddShapeButton() {
const { canvas } = useFabricKit()
const addRect = () => {
if (!canvas) return
const rect = new Rect({
left: 100, top: 100,
width: 200, height: 120,
fill: '#6366f1',
rx: 8,
})
canvas.add(rect)
canvas.setActiveObject(rect)
canvas.renderAll()
}
return <button onClick={addRect}>Add Rectangle</button>
}useContextMenu()
Builds a context-aware right-click menu based on what is selected.
const { menu, open, close } = useContextMenu({
canvas,
onUndo: undo,
onRedo: redo,
canUndo,
canRedo,
})
// Attach to canvas wrapper
useEffect(() => {
if (!canvas) return
const el = canvas.getElement().parentElement
if (!el) return
el.addEventListener('contextmenu', open)
return () => el.removeEventListener('contextmenu', open)
}, [canvas, open])Menu items shown:
- Empty canvas: Undo, Redo, Select All
- Object selected: Duplicate, Delete, Undo, Redo, Bring to Front, Send to Back
- Multi-select: + Group option
- Group selected: + Ungroup option
useExport()
Programmatic export without the UI panel. Use this when you want to wire export to your own buttons.
const {
exportPNG, // (multiplier?: number) => void
exportJPEG, // (quality?: number, multiplier?: number) => void
exportSVG, // () => void
exportJSON, // () => void
importJSON, // (jsonString: string) => Promise<void>
} = useExport('my-filename')Example:
import { useFabricKit, useExport } from 'fabric-editor-kit'
function ExportControls() {
const { canvas } = useFabricKit()
const { exportPNG, exportJSON, importJSON } = useExport('my-design')
// Save canvas state to localStorage
const handleSave = () => {
if (!canvas) return
localStorage.setItem('canvas', JSON.stringify(canvas.toJSON()))
}
// Restore canvas state from localStorage
const handleLoad = async () => {
const saved = localStorage.getItem('canvas')
if (saved) await importJSON(saved)
}
return (
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => exportPNG(2)}>Download PNG @2×</button>
<button onClick={exportJSON}>Export JSON</button>
<button onClick={handleSave}>Save to localStorage</button>
<button onClick={handleLoad}>Load from localStorage</button>
</div>
)
}Theming
createTheme(mode)
Create a light or dark theme with one call:
import { createTheme, ThemeProvider } from 'fabric-editor-kit'
const theme = createTheme('dark') // or 'light'
<ThemeProvider theme={theme}>
{/* all components inherit the theme */}
</ThemeProvider>Custom Brand Theme
Override any token to match your product's design system:
import { createTheme } from 'fabric-editor-kit'
const theme = createTheme('light', {
accentColor: '#7c3aed', // Your brand primary color
accentText: '#ffffff', // Text on accent backgrounds
toolbarBg: '#1e1e2e', // Toolbar background
toolbarBorder: '#2e2e3e', // Toolbar border
toolbarRadius: '10px', // Toolbar border radius
toolbarShadow: '0 8px 32px rgba(0,0,0,0.24)', // Toolbar shadow
textPrimary: '#f8f8f2', // Primary text
textSecondary: '#a6adc8', // Secondary/muted text
textDisabled: '#6c7086', // Disabled state text
iconColor: '#cdd6f4', // Icon color
bgHover: '#313244', // Button hover background
bgActive: '#45475a', // Button active/pressed background
bgTertiary: '#181825', // Input/select background
inputBg: '#313244', // Text input background
borderDefault: '#45475a', // Default border
borderSubtle: '#313244', // Subtle/faint border
dangerColor: '#f38ba8', // Danger/delete actions
})Available Theme Tokens
| Token | Description |
|---|---|
| accentColor | Primary brand color used on active states and buttons |
| accentText | Text color on accent-colored backgrounds |
| toolbarBg | Background of all toolbars and panels |
| toolbarBorder | Border of all toolbars and panels |
| toolbarRadius | Border radius of toolbars and panels |
| toolbarShadow | Box shadow of toolbars and panels |
| textPrimary | Main text color |
| textSecondary | Muted/label text color |
| textDisabled | Disabled state text and shortcut hints |
| iconColor | Default icon color |
| bgHover | Background on mouse hover |
| bgActive | Background on active/selected state |
| bgTertiary | Input and select backgrounds |
| inputBg | Text input field background |
| borderDefault | Standard border color |
| borderSubtle | Faint separator/divider color |
| dangerColor | Delete and destructive action color |
Keyboard Shortcuts
All shortcuts work when the canvas wrapper is focused.
| Shortcut | Action |
|---|---|
| Ctrl+Z | Undo |
| Ctrl+Y / Ctrl+Shift+Z | Redo |
| Delete / Backspace | Delete selected object(s) |
| Ctrl+D | Duplicate selected |
| Ctrl+A | Select all objects |
| Ctrl+G | Group selected objects |
| Ctrl+Shift+G | Ungroup selected group |
| Ctrl+] | Bring to front |
| Ctrl+[ | Send to back |
| Escape | Deselect / close context menu |
Project Structure
src/
├── core/
│ └── FabricKitProvider.tsx ← Canvas init, undo/redo, selection tracking
├── hooks/
│ ├── useFabricKit.ts ← Access canvas context
│ └── useExport.ts ← Programmatic export
├── theme/
│ ├── ThemeProvider.tsx ← Theme context
│ ├── createTheme.ts ← Theme factory function
│ └── tokens.ts ← Default light/dark tokens
├── toolbars/
│ ├── CanvasToolbar.tsx ← No-selection toolbar
│ ├── TextToolbar.tsx ← Text object toolbar
│ ├── ShapeToolbar.tsx ← Shape object toolbar
│ ├── ImageToolbar.tsx ← Image object toolbar
│ ├── MultiToolbar.tsx ← Multi-select toolbar
│ └── shared/ ← ToolbarButton, ToolbarInput etc.
├── context-menu/
│ ├── ContextMenu.tsx ← Menu renderer
│ ├── useContextMenu.tsx ← Menu builder hook
│ └── types.ts ← ContextMenuItem interface
└── panels/
├── LayerPanel.tsx ← Layer management panel
└── ExportPanel.tsx ← Export/import panelLicense
MIT © fabric-editor-kit contributors
