This guide shows how to save a Fabric.js canvas as JSON and load it back. You keep your own canvas, toolbar and UI. The engine only adds the document work around them.
Install
The package works with Fabric.js 6 and 7. Install it from npm next to fabric:
npm install fabricjs-document-engine fabricSave and load
Connect the engine to the canvas you already have. toDocument() returns the whole canvas as plain JSON, and loadDocument() puts it back:
import { Canvas, Rect } from "fabric";
import { createDocumentEngine } from "fabricjs-document-engine";
const canvas = new Canvas("editor", { width: 800, height: 600 });
const engine = createDocumentEngine({ canvas });
canvas.add(new Rect({ width: 100, height: 80, fill: "tomato" }));
const document = engine.toDocument();
localStorage.setItem(document.id, JSON.stringify(document));
await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));Try it below. Add a few shapes, save a Fabric.js canvas as JSON with the button, move things around, then load from JSON to get the saved state back.
Loading the live demo. Its code is below.
"use client";
import type { FabricDocument } from "fabricjs-document-engine";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { CircleIcon, FolderOpenIcon, SaveIcon, SquareIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import {
DemoCanvas,
DemoFrame,
newShape,
seedShapes,
Status,
Toolbar,
ToolButton,
} from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
export default function SaveLoadDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const [saved, setSaved] = useState<FabricDocument>();
useEffect(() => {
if (!engine) return;
engine.canvas.add(...seedShapes());
engine.clearHistory();
}, [engine]);
function save() {
// The whole canvas as plain JSON. Store it anywhere.
setSaved(engine?.toDocument());
}
async function load() {
if (saved) await engine?.loadDocument(saved);
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={SquareIcon}
label={t("square")}
onClick={() => canvas?.add(newShape("square"))}
/>
<ToolButton
icon={CircleIcon}
label={t("circle")}
onClick={() => canvas?.add(newShape("circle"))}
/>
<span className="ml-auto flex gap-1.5">
<ToolButton
icon={SaveIcon}
label={t("saveLoad.save")}
primary
disabled={!engine}
onClick={save}
/>
<ToolButton
icon={FolderOpenIcon}
label={t("saveLoad.load")}
disabled={!saved}
onClick={() => void load()}
/>
</span>
</Toolbar>
<Status tone={saved ? "ok" : "idle"}>
{saved
? t("saveLoad.saved", { count: saved.objects.length })
: t("saveLoad.hint")}
</Status>
<pre className="h-32 overflow-auto rounded-lg border bg-muted p-3 font-mono text-xs">
{saved ? JSON.stringify(saved, null, 2) : "{}"}
</pre>
</DemoFrame>
);
}Every object in the saved JSON has a stable id. It survives moving, grouping, saving and reopening, so you can link objects to your own data.
What you get for free
Creating the engine also turns on undo redo. Every user action, such as adding, moving or resizing a shape, becomes one undo step:
await engine.undo();
await engine.redo();Save to storage
For real apps, give the engine a storage adapter. Then save() and load(id) work, and autosave keeps the document saved while the user edits:
import { createDocumentEngine } from "fabricjs-document-engine";
import { createLocalStorage } from "fabricjs-document-engine/storage";
const engine = createDocumentEngine({
canvas,
storage: createLocalStorage({ prefix: "my-app:" }),
autosave: true,
});
await engine.load("project-42");
await engine.save();The same code works when you save a Fabric.js canvas as JSON to your own database. See storage adapters.
Next steps
- Editor tutorial: build a small canvas editor step by step.
- Save and load: custom properties, images and loading rules.
- Undo and redo: transactions and keyboard shortcuts.
- React: hooks for your toolbar.