Why pair GrapesJS with Svelte
GrapesJS is a framework-agnostic, MIT-licensed drag-and-drop builder, so it
drops into a SvelteKit app without a wrapper library. The only rule to respect
is that GrapesJS needs the DOM — it must run in the browser, not during
server-side rendering. Svelte's onMount lifecycle makes that
trivial: it only fires on the client, which is exactly where the editor belongs.
This guide covers a production-ready setup: install, mount the editor safely, persist content to your backend, and export clean HTML and CSS.
1. Install GrapesJS
npm install grapesjs
No Svelte-specific adapter is required. You can add presets later (for example
grapesjs-preset-webpage or the
plugins from GJS.Market) once the core editor runs.
2. Mount the editor client-side
Create a route, e.g. src/routes/editor/+page.svelte. Bind a
container element, then initialise GrapesJS inside onMount and
return its teardown so the instance is destroyed on navigation.
<script>
import { onMount } from 'svelte';
import grapesjs from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';
let container; // bound to the editor div
let editor; // GrapesJS instance
onMount(() => {
editor = grapesjs.init({
container,
height: '100vh',
fromElement: false,
storageManager: false, // we wire storage manually in step 3
components: '<h1>Hello from GrapesJS</h1>',
style: 'h1 { font-family: sans-serif; }',
});
// Clean up listeners + DOM when the component unmounts.
return () => editor?.destroy();
});
</script>
<div bind:this={container}></div>
Because onMount never runs on the server, the editor is created
only in the browser — no SSR errors, no window is not defined.
3. Persist content to your backend
The simplest approach is a save button that reads the current document and POSTs it to your API:
<script>
async function save() {
const payload = {
html: editor.getHtml(),
css: editor.getCss(),
// Full editable project (for re-loading into the editor later):
project: editor.getProjectData(),
};
await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
</script>
<button on:click={save}>Save</button>
Prefer automatic saves? Configure the Storage Manager instead of
storageManager: false:
storageManager: {
type: 'remote',
stepsBeforeSave: 3,
options: {
remote: {
urlStore: '/api/pages',
urlLoad: '/api/pages/load',
},
},
}
4. Export production HTML and CSS
editor.getHtml() and editor.getCss() return the
rendered markup and styles as strings. Combine them when you need a standalone
document:
const html = editor.getHtml();
const css = editor.getCss();
const document = `<!doctype html>
<html>
<head><style>${css}</style></head>
<body>${html}</body>
</html>`;
Reload a saved project
To re-open a page in the editor, pass the stored project data back in via
editor.loadProjectData(saved.project) after init, or set
projectData in the init config.
Prerequisites
Before you start, make sure you have Node.js 18 or newer and a SvelteKit
project created with npm create svelte@latest. You don't need any
Svelte-specific GrapesJS wrapper — GrapesJS is framework-agnostic and ships as a
plain ES module, so the same approach works in a SvelteKit app, a plain Svelte
SPA, or a Svelte component embedded in a larger page. A basic understanding of
Svelte's lifecycle (onMount, onDestroy) and of where
code runs (server vs browser) is all you need to follow along.
Add custom blocks to the editor
A bare editor isn't very useful until your users have building blocks to drag
onto the canvas. Register them with the Block Manager right after
grapesjs.init. Each block is a label plus the HTML (or a component
definition) it drops onto the page:
editor.BlockManager.add('hero', {
label: 'Hero section',
category: 'Sections',
content: `
<section class="hero">
<h1>Headline</h1>
<p>Supporting copy goes here.</p>
<a href="#">Call to action</a>
</section>`,
});
editor.BlockManager.add('two-cols', {
label: 'Two columns',
category: 'Layout',
content: '<div class="row"><div class="cell"></div><div class="cell"></div></div>',
});
Group related blocks with the category property so the panel stays
organised, and reach for ready-made block libraries and presets from GJS.Market
when you want a richer starting set without building every block by hand.
Persisting to a real backend (deep-dive)
In a SvelteKit app the natural place to receive a save is a
+server.js endpoint. Wire the Storage Manager to it and send your
auth token in the request headers:
// in grapesjs.init(...)
storageManager: {
type: 'remote',
autosave: true,
stepsBeforeSave: 3,
options: {
remote: {
urlStore: '/api/pages',
urlLoad: '/api/pages',
headers: { Authorization: `Bearer ${token}` },
},
},
}
// src/routes/api/pages/+server.js
import { json } from '@sveltejs/kit';
let page = {}; // swap for your database
export async function GET() {
return json(page);
}
export async function POST({ request }) {
page = await request.json(); // the full GrapesJS project
return json({ status: 'ok' });
}
For full control you can register a custom storage adapter instead of the remote preset — useful when you need optimistic UI, offline queues, or a non-HTTP store:
editor.Storage.add('svelte-store', {
async load() {
const res = await fetch('/api/pages');
return res.ok ? res.json() : {};
},
async store(data) {
await fetch('/api/pages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
},
});
// then: storageManager: { type: 'svelte-store' }
Performance tips
GrapesJS is a sizeable dependency, so load it only where it's needed. Importing
it dynamically inside onMount (await import('grapesjs'))
keeps it out of your main bundle and out of the server render path. Always tear
the editor down with editor.destroy() when the component unmounts —
during SvelteKit's client-side navigation a leaked instance keeps its listeners
and DOM nodes alive, and they accumulate. Tune stepsBeforeSave so
autosave isn't firing on every tiny change, and lazy-load heavy plugins only when
a user actually opens the feature that needs them.
Security considerations
GrapesJS faithfully stores whatever markup is on the canvas, so treat exported HTML as untrusted if non-admin users can edit pages: sanitise it on output (a vetted server-side sanitiser) before rendering it to other visitors. Protect the save endpoint with real authentication and authorisation — never accept an unauthenticated POST that overwrites a page. If you load third-party plugins into the editor, pin their versions and review them, since they run with full access to the editor and the page.
Troubleshooting common errors
“document is not defined” means GrapesJS ran during SSR — move
the import and init inside onMount. The canvas
looks unstyled means the stylesheet import is missing; add
import 'grapesjs/dist/css/grapes.min.css'. The editor is
blank usually means the container element wasn't bound yet when
init ran — bind it with bind:this and init in
onMount. Saves fail with 413 means the project
payload exceeds your server's body-size limit; raise it on the endpoint.
When to use GrapesJS with Svelte
GrapesJS is the right call when you need to embed a real visual page or email builder inside your Svelte product — a SaaS page editor, a CMS surface, a newsletter composer — and you want to own the editor, its storage, and its HTML output. If all you need is rich-text editing inside a single field, a lighter rich-text component is a better fit. For full-page composition with drag-and-drop layout, a style manager, and clean HTML/CSS export, GrapesJS gives you far more than a text editor while staying MIT-licensed and self-hosted.
Common pitfalls in SvelteKit
Three mistakes account for almost every broken setup. First, importing GrapesJS at the top of the module instead of inside onMount — that runs it during SSR and throws document is not defined. Keep the import lazy (await import('grapesjs')) inside the lifecycle hook. Second, forgetting to return editor.destroy(): with SvelteKit's client-side routing the component remounts often, and leaked editor instances pile up listeners and memory. Third, missing the stylesheet — without grapesjs/dist/css/grapes.min.css the canvas renders unstyled and looks broken. Fix those three and the integration is stable across navigation, hot reloads, and production builds.
Next steps
Once the core editor runs, extend it with blocks and plugins. See the related framework guides for GrapesJS in React and GrapesJS in Vue, browse ready-made GrapesJS plugins and templates, or try the editor directly from the GJS.Market home page.
FAQ
Does GrapesJS work with SvelteKit server-side rendering?
Yes. GrapesJS needs the DOM, so mount it inside onMount, which only
runs in the browser. The route around it can still be server-rendered; only the
editor instance is client-only.
How do I save GrapesJS content in a Svelte app?
Use the Storage Manager with type: 'remote' to POST project data to
your backend, or read editor.getHtml() and
editor.getCss() on a save button and send them yourself.
Do I need to destroy the editor on unmount?
Yes. Return editor.destroy() from onMount so GrapesJS
removes its listeners and DOM nodes during client-side navigation, preventing
memory leaks.