GrapesJS in Vue 3: a script setup component with clean teardown
A Vue 3.5 single-file component that mounts GrapesJS, emits ready and change events, keeps the editor out of Vue's deep reactivity and cleans up on unmount. Compiled with Vue's own SFC compiler and tested in Chromium against GrapesJS 0.23.5.
TL;DR
- Mount GrapesJS in onMounted against a template ref and call editor.destroy() in onBeforeUnmount; toggling the component with v-if then leaves exactly one editor, which our test checks.
- Hold the editor in shallowRef, not ref: ref would convert the editor's internal objects into deep reactive proxies, which buys nothing because GrapesJS has its own event system.
- Changes flow out through the editor's update event, re-emitted as a Vue event; commands flow in through the editor API, not through props.
What you are building
A GrapesEditor.vue component for Vue 3.5 that mounts GrapesJS, exposes the
editor to its parent, and removes it without leaks. The approach is the same one the
React guide takes: GrapesJS owns its DOM and its state, Vue
provides the element and listens.
If you are still choosing an engine for a Vue product, the Vue visual editor options page compares the field first.
Install
npm install grapesjs@0.23.5The example uses Vue 3.5.43. useTemplateRef needs Vue 3.5 or later; on older 3.x versions, use a
plain ref bound with ref="container" instead.
The component
<script setup lang="ts">
import { onBeforeUnmount, onMounted, shallowRef, useTemplateRef } from 'vue';
import grapesjs, { type Editor, type ProjectData } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
const props = defineProps<{ initialProject: ProjectData }>();
const emit = defineEmits<{
ready: [editor: Editor];
change: [project: ProjectData];
}>();
const container = useTemplateRef<HTMLDivElement>('container');
// shallowRef: Vue must not wrap the editor's internals in deep reactive proxies.
const editor = shallowRef<Editor | null>(null);
onMounted(() => {
if (!container.value) return;
const instance = grapesjs.init({
container: container.value,
height: '100%',
storageManager: false,
projectData: props.initialProject,
});
instance.on('update', () => emit('change', instance.getProjectData()));
instance.onReady(() => emit('ready', instance));
editor.value = instance;
});
onBeforeUnmount(() => {
editor.value?.destroy();
editor.value = null;
});
defineExpose({ editor });
</script>
<template>
<div ref="container" class="grapes-editor" />
</template>What matters in it:
onMounted, notsetuptime. The template ref is only populated after mount.shallowReffor the editor. Vue does not need to observe the editor's internals, and deep proxies around them are wasted work at best.onBeforeUnmountdestroys. Without it, av-iftoggle or a route change leaves a detached editor with live listeners behind.storageManager: false. GrapesJS's default is autosave tolocalStorage, which is rarely what an application wants. Choose persistence explicitly; see saving to a database.
Using it
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import type { Editor, ProjectData } from 'grapesjs';
import GrapesEditor from './GrapesEditor.vue';
const initialProject: ProjectData = {
pages: [{ component: '<section><h1>Hello from Vue</h1></section>' }],
};
const editor = shallowRef<Editor | null>(null);
const changes = ref(0);
const saved = ref('');
const showEditor = ref(true);
function addSection() {
editor.value?.addComponents('<section><p>Added from a Vue button</p></section>');
}
function save() {
if (editor.value) saved.value = JSON.stringify(editor.value.getProjectData());
}
</script>
<template>
<div style="display: grid; grid-template-rows: auto 1fr; height: 100vh">
<header style="display: flex; gap: 12px; padding: 8px">
<button type="button" @click="addSection">Add section</button>
<button type="button" @click="save">Save</button>
<button type="button" @click="showEditor = !showEditor">Toggle editor</button>
<span data-testid="changes">{{ changes }}</span>
<output data-testid="saved" hidden>{{ saved }}</output>
</header>
<GrapesEditor
v-if="showEditor"
:initial-project="initialProject"
@ready="(instance) => (editor = instance)"
@change="changes++"
/>
</div>
</template>import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');If you use TypeScript with .vue imports outside Vite's templates, you need the usual module
declaration so tsc accepts them:
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent;
export default component;
}What our test checks
The verifier compiles both SFCs with vue/compiler-sfc, the compiler Vue's Vite plugin uses,
bundles them with esbuild, and runs the result in headless Chromium. It asserts that:
- the initial project renders inside the canvas iframe;
- a Vue button calling
editor.addComponents()produces achangeevent the parent counts; getProjectData()contains the added section;- hiding the component with
v-ifremoves every.gjs-editorelement, and showing it again creates exactly one; - no errors reach the browser console.
What this guide does not cover
- Vue components on the canvas. The canvas renders HTML. Your Vue components are not editable units in GrapesJS, and wrapping them is a different project.
- Nuxt and server rendering. Untested; see the FAQ.
- A Vue-rendered editor UI. The example keeps GrapesJS's default panels. Replacing them is the bulk of the work in a customer-facing product, and the reason the GrapesJS vs Studio SDK comparison exists.
- Options API. The component uses
<script setup>; the same lifecycle calls map tomountedandbeforeUnmount, but we did not test that variant.
Frequently asked questions
- Is there an official Vue wrapper for GrapesJS?
- Not that we found. The GrapesJS organisation publishes @grapesjs/react on npm, but the npm registry had no @grapesjs/vue package when we checked on 17 September 2026. The component in this guide is 39 lines, small enough that a wrapper would add more dependency than it removes.
- Why shallowRef instead of ref for the editor?
- Vue's ref makes objects deeply reactive by wrapping them in proxies. The GrapesJS editor is a large object graph that manages its own state and events. shallowRef tracks only reassignment of .value, which is all a component needs to know.
- Does this work with Nuxt?
- The component itself should, rendered client-side only, but we have not tested it in Nuxt. GrapesJS initialises in onMounted, which does not run during server rendering.
Sources
- Vue documentation: Reactivity API, Advanced (shallowRef) — accessed 2026-09-17
- Vue documentation: Composition API Helpers (useTemplateRef) — accessed 2026-09-17
- GrapesJS API reference: Editor — accessed 2026-09-17
- npm registry metadata for vue — accessed 2026-09-17
Written by the EditorStack Research Team. How we test and what we refuse to publish is on the methodology page.