Vue / Quasar / Nuxt Integration
Design on Web does not ship a dedicated Vue wrapper, but you can use the @design-on-web/vanilla package directly in any Vue 3 + Vite project (including Quasar, Nuxt, and Vitepress). The example below mirrors the structure of the React integration but uses the framework-agnostic API.
@design-on-web/vanilla for canvas operations. The trade-off is more setup, but you get full control over the look-and-feel.1. Installation
Install the vanilla package plus Fabric.js as a peer dependency:
pnpm add @design-on-web/vanilla @design-on-web/core fabric
# Vite + Vue 3 starter
pnpm create vite my-editor --template vue-ts
cd my-editor
pnpm install2. Vite Configuration
When consuming the SDK as a linked or workspace dependency (e.g. in a monorepo), exclude it from Vite's dep optimizer so source changes trigger HMR:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
// Critical when consuming locally linked packages — Vite must
// serve them directly so dist updates trigger HMR.
exclude: [
'@design-on-web/core',
'@design-on-web/shared',
'@design-on-web/vanilla',
],
},
});link:../path/to/package not file:../path/to/package. The file: protocol copies the package into node_modules/.pnpm, so rebuilding the SDK's dist/ output is not picked up. link: creates a real symlink and HMR works correctly.3. Minimal Vue Component
A complete editor in a single Vue file. The canvas host is a plain<div> — Fabric.js manages the inner canvas element. Smart guides are enabled after initWorkspace so the canvas is properly mounted.
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue';
import { createEditor } from '@design-on-web/vanilla';
import type { EditorAPI, EditorEngine } from '@design-on-web/core';
const canvasHost = ref<HTMLDivElement | null>(null);
const engine = shallowRef<EditorAPI | null>(null);
onMounted(() => {
if (!canvasHost.value) return;
const api = createEditor(canvasHost.value, {
width: 1080,
height: 1080,
backgroundColor: '#ffffff',
onReady: (editor) => {
// Resize the workspace to fill the host THEN enable smart guides
// (smart guides need contextTop, which exists once the canvas
// is mounted in the DOM with valid dimensions).
const eng = editor as unknown as EditorEngine;
const cm = eng.getCanvasManager();
const rect = canvasHost.value!.getBoundingClientRect();
cm.initWorkspace(rect.width, rect.height, '#ffffff');
eng.enableSmartGuides();
// Seed sample content
editor.addShape('rect', {
left: 280, top: 320, width: 320, height: 200,
fill: '#8b3dff', rx: 12, ry: 12,
});
},
});
engine.value = api;
});
onBeforeUnmount(() => {
if (engine.value) {
(engine.value as unknown as EditorEngine).destroy();
engine.value = null;
}
});
function addCircle() {
engine.value?.addShape('circle', {
left: 400, top: 400, radius: 80, fill: '#ec4899',
});
}
async function downloadPng() {
if (!engine.value) return;
const blob = await engine.value.exportAs('png');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'design.png';
a.click();
URL.revokeObjectURL(url);
}
</script>
<template>
<div style="display: flex; flex-direction: column; height: 100vh">
<header style="padding: 12px; background: #1a1a1a; color: #fff">
<button @click="addCircle">+ Circle</button>
<button @click="downloadPng">Download PNG</button>
</header>
<main style="flex: 1; position: relative">
<div ref="canvasHost" style="position: absolute; inset: 0"></div>
</main>
</div>
</template>4. Building a Layer Panel in Vue
Subscribe to canvas events to keep a Vue ref in sync with the object list. The pattern below works for any custom panel (properties, history, pages, etc.):
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { createEditor } from '@design-on-web/vanilla';
import type { EditorAPI, EditorEngine } from '@design-on-web/core';
interface LayerInfo {
id: string;
name: string;
type: string;
visible: boolean;
isActive: boolean;
}
const layers = ref<LayerInfo[]>([]);
const engine = ref<EditorAPI | null>(null);
function refreshLayers(api: EditorAPI) {
const fabricCanvas = api.getCanvas() as {
getObjects: () => unknown[];
getActiveObject: () => unknown;
};
const activeObj = fabricCanvas.getActiveObject() as { id?: string } | null;
layers.value = (fabricCanvas.getObjects() as Array<{
id?: string;
name?: string;
type?: string;
visible?: boolean;
_isWorkspace?: boolean;
_isGridLine?: boolean;
}>)
.filter((o) => !o._isWorkspace && !o._isGridLine && o.id)
.map((o) => ({
id: o.id!,
name: o.name ?? o.type ?? 'Object',
type: o.type ?? 'unknown',
visible: o.visible !== false,
isActive: o.id === activeObj?.id,
}))
.reverse();
}
onMounted(() => {
// ... createEditor setup ...
// Subscribe to canvas events for live layer list
const eng = engine.value as unknown as EditorEngine;
const events = (eng as unknown as { eventBus: { on: Function } }).eventBus;
const fabricCanvas = engine.value!.getCanvas() as {
on: (e: string, cb: () => void) => void;
};
const onChange = () => refreshLayers(engine.value!);
events.on('object:added', onChange);
events.on('object:removed', onChange);
events.on('object:modified', onChange);
fabricCanvas.on('selection:created', onChange);
fabricCanvas.on('selection:updated', onChange);
fabricCanvas.on('selection:cleared', onChange);
});
function selectLayer(id: string) {
engine.value?.selectObject(id);
}
function deleteLayer(id: string) {
engine.value?.removeObject(id);
}
</script>
<template>
<aside class="layer-panel">
<h3>Layers ({{ layers.length }})</h3>
<div
v-for="layer in layers"
:key="layer.id"
:class="{ active: layer.isActive }"
@click="selectLayer(layer.id)"
>
{{ layer.name }}
<button @click.stop="deleteLayer(layer.id)">🗑</button>
</div>
</aside>
</template>5. SSR Frameworks (Nuxt / Quasar)
Fabric.js requires document and window, so the editor must only run on the client. Wrap your editor component in a client-only boundary:
// Quasar / Nuxt — wrap in client-only component
// because Fabric.js needs DOM access.
// Quasar:
<template>
<q-no-ssr>
<DesignEditor />
</q-no-ssr>
</template>
// Nuxt 3:
<template>
<ClientOnly>
<DesignEditor />
</ClientOnly>
</template>What you get from the EditorAPI
The createEditor return value is the same EditorAPI used by the React integration. From your Vue components you can call:
addShape(type, options),addText(text, options),addImage(src, options)selectObject(id),removeObject(id),getSelectedObjects()undo(),redo(),canUndo(),canRedo()alignObjects(),distributeObjects(),groupSelected(),ungroupSelected()exportAs(format, options)— returns aBlobfor any of 8 formatsgetCanvas()— escape hatch to the underlying Fabric.jsCanvasfor advanced operations
Reference Implementation
A complete Vue + Vite reference app with Canva-like UI (left rail, shape grid, layer panel, property editor, zoom controls, export dialog) lives at design-on-web-test-vue/ in the project sources. It uses ~600 lines of Vue + 400 lines of CSS to build a full editor on top of the vanilla API.