The usual way to load SVG into Fabric.js is loadSVGFromString followed by util.groupSVGElements, which sizes the group to what is drawn. An element outside the SVG's viewBox, or a hidden one, then moves and resizes the whole artwork. A Fabric.js import SVG through the engine keeps the file's own frame instead.
Loading the live demo. Its code is below.
"use client";
import { type FabricObject, loadSVGFromString, util } from "fabric";
import { useDocumentEngine } from "fabricjs-document-engine/react";
import { FileUpIcon, ImportIcon, ScissorsIcon, ShapesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";
import { DemoCanvas, DemoFrame, Status, Toolbar, ToolButton } from "./demo-ui";
import { useFabricCanvas } from "./use-fabric-canvas";
/**
* A badge drawn in a 240 × 150 viewBox, plus one shape far outside it and
* one hidden shape. Grouping by content lets those two move the badge.
*/
const SAMPLE = `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="150" viewBox="0 0 240 150">
<rect x="10" y="10" width="220" height="130" rx="18" fill="#ffd666"/>
<circle cx="70" cy="75" r="36" fill="#f2836f"/>
<rect x="124" y="52" width="86" height="16" rx="8" fill="#0d0d0d"/>
<rect x="124" y="82" width="60" height="16" rx="8" fill="#6f00ff"/>
<circle cx="900" cy="600" r="40" fill="#5fe6c4"/>
<rect x="-500" y="-300" width="40" height="40" fill="#7ad9ec" style="display:none"/>
</svg>`;
export default function SvgImportDemo() {
const t = useTranslations("demos");
const { elementRef, canvas } = useFabricCanvas();
const engine = useDocumentEngine(canvas);
const fileRef = useRef<HTMLInputElement>(null);
const [message, setMessage] = useState("");
function clear() {
if (!engine) return;
engine.canvas.remove(...engine.canvas.getObjects());
}
// The usual way: Fabric's parser, then a group sized to the content.
async function importWithFabric() {
if (!engine) return;
clear();
const { objects, options } = await loadSVGFromString(SAMPLE);
const group = util.groupSVGElements(
objects.filter(Boolean) as FabricObject[],
options
);
group.set({ left: 120, top: 75, originX: "left", originY: "top" });
engine.canvas.add(group);
setMessage(t("svgImport.fabric"));
}
async function importSvg(svg: string, offscreen: "keep" | "clip") {
if (!engine) return;
clear();
try {
const { warnings } = await engine.importSvg(svg, {
left: 120,
top: 75,
offscreen,
});
setMessage(
warnings.length > 0
? t("svgImport.warnings", { count: warnings.length })
: t("svgImport.engine")
);
} catch (error) {
setMessage(t("svgImport.failed", { message: String(error) }));
}
}
async function openFile(file: File | undefined) {
if (!engine || !file) return;
clear();
try {
await engine.importSvg(await file.text(), {
left: 20,
top: 20,
fit: { width: 440, height: 260 },
});
setMessage(t("svgImport.file", { name: file.name }));
} catch (error) {
setMessage(t("svgImport.failed", { message: String(error) }));
}
}
return (
<DemoFrame>
<DemoCanvas elementRef={elementRef} label={t("canvas")} />
<Toolbar>
<ToolButton
icon={ImportIcon}
label={t("svgImport.withEngine")}
primary
disabled={!engine}
onClick={() => void importSvg(SAMPLE, "keep")}
/>
<ToolButton
icon={ShapesIcon}
label={t("svgImport.withFabric")}
disabled={!engine}
onClick={() => void importWithFabric()}
/>
<ToolButton
icon={ScissorsIcon}
label={t("svgImport.clip")}
disabled={!engine}
onClick={() => void importSvg(SAMPLE, "clip")}
/>
<ToolButton
icon={FileUpIcon}
label={t("svgImport.open")}
disabled={!engine}
onClick={() => fileRef.current?.click()}
/>
<input
ref={fileRef}
type="file"
accept=".svg,image/svg+xml"
className="sr-only"
tabIndex={-1}
aria-hidden
onChange={(event) => {
void openFile(event.target.files?.[0]);
event.target.value = "";
}}
/>
</Toolbar>
<Status tone={message ? "ok" : "idle"}>
{message || t("svgImport.hint")}
</Status>
</DemoFrame>
);
}Import an SVG file
const { objects, viewport, warnings } = await engine.importSvg(svgText, {
left: 40,
top: 40,
});- Elements land where the SVG puts them, after
viewBoxandpreserveAspectRatio. - The result is one group with a fixed layout the size of the viewport, so nothing inside can move it.
- The import is one undo step, and every object gets an id.
viewportis the size of the SVG's frame, which helps when you add SVG to canvas at a chosen size.
To open a file the user picks:
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (file) await engine.importSvg(await file.text(), { fit: { width: 400, height: 300 } });
});Place and fit the artwork
| Option | Values | Default |
|---|---|---|
left, top | Where the viewport's top-left corner lands | 0 |
fit | { width, height, mode }, mode is 'contain', 'cover' or 'fill' | none |
offscreen | 'keep', 'drop' or 'clip' | 'keep' |
as | 'group' or 'objects' | 'group' |
viewport | 'preserve' or 'content' | 'preserve' |
offscreen: "drop"leaves out elements entirely outside the viewport, and"clip"hides what lies outside it.as: "objects"adds separate objects in the same places, for files the user will take apart.viewport: "content"uses the bounds of what is drawn, like Fabric's own grouping. An SVG with no size always uses it.
Untrusted files
Every Fabric.js import SVG call cleans the file before Fabric parses it:
- Scripts, event handlers such as
onload,foreignObjectand links to other files are removed. - Image addresses that
limits.isAllowedUrlrefuses are left out. - An SVG with too many elements, or nested too deep, is refused with
UNSAFE_DOCUMENT, using the samelimitsas documents. - Text that is not an SVG rejects with
SVG_IMPORT_FAILED.
warnings lists what was removed or dropped, with the codes SVG_CONTENT_REMOVED, SVG_IMAGE_BLOCKED and SVG_OFFSCREEN_DROPPED. To export the canvas back to SVG, see SVG export. The options are listed with the DocumentEngine methods, and the safety limits on the security page.