# GrapesJS in Vue 3: a script setup component with clean teardown

*Source: https://www.editorstack.cc/guides/grapesjs-vue — last updated 2026-09-17. Licensed CC BY 4.0 with attribution to EditorStack.*

*Tested with GrapesJS 0.23.5. The code on this page runs in CI from examples/guides/grapesjs-vue in the site repository.*

## Summary

- 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](/libraries/grapesjs), exposes the
editor to its parent, and removes it without leaks. The approach is the same one the
[React guide](/guides/grapesjs-react-integration) 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](/use-cases/vue-visual-editor-options) page compares the field first.

## Install

```bash
npm install grapesjs@0.23.5
```

The 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

```vue title="GrapesEditor.vue"
<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`, not `setup` time.** The template ref is only populated after mount.
- **`shallowRef` for the editor.** Vue does not need to observe the editor's internals, and deep
  proxies around them are wasted work at best.
- **`onBeforeUnmount` destroys.** Without it, a `v-if` toggle or a route change leaves a detached
  editor with live listeners behind.
- **`storageManager: false`.** GrapesJS's default is autosave to `localStorage`, which is rarely
  what an application wants. Choose persistence explicitly; see
  [saving to a database](/guides/grapesjs-save-load-database).

## Using it

```vue title="App.vue"
<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>
```

```ts title="main.ts"
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:

```ts title="env.d.ts"
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:

1. the initial project renders inside the canvas iframe;
2. a Vue button calling `editor.addComponents()` produces a `change` event the parent counts;
3. `getProjectData()` contains the added section;
4. hiding the component with `v-if` removes every `.gjs-editor` element, and showing it again
   creates exactly one;
5. no errors reach the browser console.

> **Sponsored — GJS.Market:** GrapesJS for Vue 3 projects — GJS.Market's Vue page gathers plugins, presets and templates for building a GrapesJS editor into a Vue app. GJS.Market funds this site; confirm each plugin supports your GrapesJS version.

## 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](/compare/grapesjs-vs-grapesjs-studio-sdk) exists.
- **Options API.** The component uses `<script setup>`; the same lifecycle calls map to `mounted`
  and `beforeUnmount`, 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

1. [Vue documentation: Reactivity API, Advanced (shallowRef)](https://vuejs.org/api/reactivity-advanced.html) — accessed 2026-09-17
2. [Vue documentation: Composition API Helpers (useTemplateRef)](https://vuejs.org/api/composition-api-helpers.html) — accessed 2026-09-17
3. [GrapesJS API reference: Editor](https://grapesjs.com/docs/api/editor.html) — accessed 2026-09-17
4. [npm registry metadata for vue](https://registry.npmjs.org/vue) — accessed 2026-09-17
