PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access

Step-by-step tutorial

GrapesJS Tutorial: Build Your First Visual Editor

Learn GrapesJS step by step. Build your first visual editor, add blocks and custom components, configure styles and assets, save projects, export HTML/CSS, install plugins, and integrate GrapesJS with React, Vue, Angular or Next.js.

  • Open-source editor framework
  • Self-hostable
  • Extensible with plugins
  • Custom components
  • HTML/CSS export
  • React / Vue / Angular / vanilla JS

GrapesJS core is published under the BSD-3-Clause licence; the official React wrapper @grapesjs/react is MIT. Both permit commercial use. Every example below was checked against GrapesJS 0.23.6.

The result

What You'll Build

By the end of this tutorial, you'll have a working drag-and-drop visual editor that can create pages, edit components, manage styles and assets, save projects, and export the resulting HTML and CSS.

  • Drag blocks onto a canvas
  • Select and edit components
  • Restyle anything from a Style Manager
  • Switch between desktop, tablet and phone widths
  • Read the project back as JSON
  • Export the page as HTML and CSS

Estimated learning path

Beginner → Intermediate → Production

How long that takes depends entirely on how much of the production layer your product needs. Steps 1–6 are an afternoon; steps 7–12 are the work.

Open the official GrapesJS demo
Prerequisites

Before You Start

Very little. If you can write a page by hand, you can follow this.

Basic HTML and CSS

You should recognise a tag, a class and a CSS property. GrapesJS edits HTML and CSS — nothing more exotic.

Basic JavaScript

Enough to read an object literal and a function. Every example here is plain JavaScript.

Node.js and npm

Only if you install from npm. The CDN route in step 1 needs nothing but a text editor and a browser.

You do not need any previous GrapesJS experience, and you do not need a framework. React, Vue, Angular and Next.js are covered in step 11, after the core is clear.

Beginner

1. Install GrapesJS

GrapesJS is a library you add to your own application, not a service you sign up for. There are two ways in, and the one you pick has no effect on any later step.

From npm

This is the route to take if you have a build step of any kind. It installs the editor and its stylesheet into your project; nothing is fetched from a third-party host at runtime.

terminal
npm install grapesjs

From a CDN

No build tooling at all: two tags in an HTML file and you have an editor. Perfect for a first look or a prototype. For anything you ship, pin an exact version rather than tracking the latest release.

index.html
<link
  rel="stylesheet"
  href="https://unpkg.com/grapesjs/dist/css/grapes.min.css"
/>
<script src="https://unpkg.com/grapesjs"></script>

<div id="gjs"></div>

<script>
  // The UMD build puts the library on window.grapesjs
  const editor = grapesjs.init({ container: '#gjs' });
</script>

What you just installed

The editor core

The canvas, the component tree, drag-and-drop, undo/redo, the panels and the managers for blocks, styles, assets, traits, layers and storage.

A stylesheet

grapesjs/dist/css/grapes.min.css — the editor's own chrome. Without it you get a working editor that looks broken.

No blocks

The core ships an empty block palette. The familiar column/text/image blocks come from a plugin, which is step 3.

No backend

No accounts, no database, no hosting. GrapesJS runs in the browser and hands data to your application, which is step 7.

Where GrapesJS sits in your application

It is a client-side component. Your application authenticates the user, decides which project to open, mounts the editor into a DOM element, and receives the project data back when it is saved. Everything above and below the editor is yours.

Beginner

2. Create Your First GrapesJS Editor

The smallest useful GrapesJS application is an empty element and one call. Copy both blocks below into a page and you have a working editor.
index.html
<!-- The editor takes over this element completely.
     Do not render anything inside it yourself. -->
<div id="gjs"></div>
One element. GrapesJS replaces its contents entirely, so never render your own UI inside it.
editor.js
import grapesjs from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

const editor = grapesjs.init({
  // Where the editor mounts: a selector or an HTMLElement.
  container: '#gjs',
  height: '100vh',
  width: 'auto',

  // Do not adopt the markup already inside #gjs...
  fromElement: false,
  // ...load this instead. Strings are parsed into components.
  components: `
    <section class="hero">
      <h1>Hello GrapesJS</h1>
      <p>Drag a block from the panel on the right.</p>
    </section>`,
  style: `
    .hero { padding: 64px 32px; font-family: system-ui, sans-serif; }
    .hero h1 { margin: 0 0 12px; font-size: 40px; }`,

  // Storage is ON by default and writes to localStorage.
  // Turn it off until you have decided where projects really live.
  storageManager: false,
});
Two options here are worth knowing on day one: fromElement decides whether the editor adopts the markup already inside the container, and storageManager defaults to a localStorage-backed store that quietly saves the canvas in the reader's browser. Turning it off now avoids a confusing amount of ghost state later.

What each part means

container
The element the editor mounts into — a CSS selector or an HTMLElement. Give it a real height, or the editor renders zero pixels tall.
editor instance
What init() returns. Every API in this tutorial hangs off it, and calling destroy() on it releases the DOM and the listeners.
canvas
The iframe your page is edited inside. Because it is a real iframe, your page's CSS cannot leak into it, and its CSS cannot leak out.
initial project
What the canvas starts with — the components and style options, or whatever the storage layer loads.
configuration
A single object. Every manager in the later steps is configured from a key on it: blockManager, styleManager, assetManager, storageManager, deviceManager.

What you should see

A three-part interface: the canvas in the middle, the panel switcher top right, and — once you have added blocks in step 3 — a palette to drag from. The editor above this section is exactly this configuration with six blocks and three device widths added.

Open Live Demo
Beginner

3. Add Drag-and-Drop Blocks

Before any code, one distinction. It is the single thing beginners get wrong most often, and everything from here on depends on it.

Block

A palette entry. It exists only in the panel and holds a recipe for what to create. It has a label, a category, an icon, and content.

Component

A node in the canvas. It has a type, attributes, styles, children, and it is what gets exported and saved.

A Block is what the user drags into the canvas. Once dropped, it creates Components inside the editor. One block can create a whole subtree of components — and the same block dropped twice creates two independent subtrees.

A starter palette

  • Hero
  • Image
  • Text
  • Button
  • Two Columns
  • Contact Form
blocks.js
// A Block is a palette entry. Dropping it creates Components.
editor.Blocks.add('hero-section', {
  label: 'Hero',
  category: 'Sections',
  // Shown in the palette. Any HTML string works; an inline SVG keeps it sharp.
  media: '<svg viewBox="0 0 24 24" width="22"><rect x="3" y="5" width="18" height="6" rx="1" fill="currentColor"/><rect x="3" y="13" width="11" height="3" rx="1" fill="currentColor" opacity=".5"/></svg>',
  content: `
    <section class="hero">
      <h1>Headline</h1>
      <p>Supporting copy.</p>
      <a href="#" class="btn">Call to action</a>
    </section>`,
});

// The same block, expressed as a component definition instead of HTML.
// Use this form once you have your own component types (step 10).
editor.Blocks.add('product-card', {
  label: 'Product card',
  category: 'Commerce',
  content: { type: 'product-card' },
});
content accepts either an HTML string or a component definition. Strings are the fastest way to start; the object form is what you switch to once you have your own component types in step 10.

The core ships no blocks

This surprises almost everyone. An out-of-the-box GrapesJS editor has an empty palette — the familiar "1 column / 2 columns / text / image" set that appears in every screenshot comes from grapesjs-blocks-basic or one of the presets. You either write your own blocks, as above, or add a preset.

blocks-preset.js
import grapesjs, { usePlugin } from 'grapesjs';
import blocksBasic from 'grapesjs-blocks-basic';

// The core ships no blocks at all. The familiar
// "1 column / 2 columns / text / image" palette is a plugin.
grapesjs.init({
  container: '#gjs',
  plugins: [usePlugin(blocksBasic, { flexGrid: true })],
});
usePlugin() is the current way to register a plugin. Older tutorials call grapesjs.plugins.add(); that API is deprecated and logs a warning.
Beginner

4. Understand GrapesJS Components

The canvas is not a string of HTML that GrapesJS parses on save. It is a live tree of Component models, and the HTML is generated from that tree. Once that clicks, the rest of the API stops being surprising.

The vocabulary

Component tree
Every node in the canvas is a component, and every component has a parent and children. The root is the wrapper.
Component types
Built-in types include text, image, link, video, table and a generic default. A type decides how a node behaves, renders and exports.
Nested components
Containment is a real relationship, not indentation. droppable and draggable control what may go inside what.
Attributes
HTML attributes — class, href, id, data-*. They end up verbatim in the exported markup.
Properties
Model state that is not an HTML attribute. Useful for anything the editor needs to remember but the page should not carry.
Traits
The settings panel for the selected component. A trait edits an attribute by default, or a property when you set changeProp.

A typical subtree

  • wrapper, depth 0
  • Section, depth 1
  • Container, depth 2
  • Heading, depth 3
  • Button, depth 3
The Layer Manager shows exactly this. Selecting a node in the canvas selects it in the tree, and vice versa.
components.js
// Every node in the canvas is a Component, and the canvas is a tree.
const wrapper = editor.getWrapper();

wrapper.components().forEach((component) => {
  console.log(
    component.get('type'),      // 'text' | 'image' | 'link' | your own type
    component.getName(),        // label shown in the Layer manager
    component.components().length // number of children
  );
});

// React to what the user selects — the hook most custom UI hangs off.
editor.on('component:selected', (component) => {
  console.log('selected', component.getId(), component.get('type'));
});
getWrapper() is the root of the tree. From there, components() gives you the children of any node — which is how every custom panel, exporter and validator in a production editor walks the document.
cta-button.js
// Traits are the settings panel for a component.
// By default a trait writes an HTML attribute.
editor.Components.addType('cta-button', {
  extend: 'link',
  model: {
    defaults: {
      name: 'CTA button',
      attributes: { class: 'btn' },
      components: 'Call to action',
      traits: [
        { name: 'href', label: 'Link' },
        { name: 'title', label: 'Title' },
        {
          type: 'select',
          name: 'target',
          label: 'Opens in',
          options: [
            { id: '', name: 'Same tab' },
            { id: '_blank', name: 'New tab' },
          ],
        },
      ],
    },
  },
});
extend inherits everything from an existing type and overrides only what you name. It is almost always the right starting point: a link that behaves like a link, with your own traits on top.
Beginner

5. Configure the Style Manager

The Style Manager writes CSS rules for whatever is selected. Left at its defaults it offers a large slice of CSS to whoever happens to be using your editor — which is fine for a developer tool and wrong for almost every product.

Sectors, and what usually goes in them

Typography
font-family, font-size, font-weight, line-height, colour, alignment.
Spacing
margin and padding, usually the two properties authors actually reach for.
Dimension
width, max-width, height and their min/max variants.
Decorations
Background colour and images, border radius, borders, shadows.
Responsive
Styles are written per device. Switch the canvas to tablet or phone and the same control writes into a media query instead.
Custom properties
You can define your own property types — a token picker, a spacing scale — rather than exposing raw CSS.
style-manager.js
grapesjs.init({
  container: '#gjs',
  styleManager: {
    // Sectors are the collapsible groups in the right-hand panel.
    // Listing them yourself is how you stop the editor offering
    // 100+ CSS properties to a non-technical author.
    sectors: [
      {
        id: 'typography',
        name: 'Typography',
        open: true,
        properties: [
          'font-family',
          'font-size',
          'font-weight',
          'line-height',
          'color',
          'text-align',
        ],
      },
      { id: 'spacing', name: 'Spacing', properties: ['margin', 'padding'] },
      {
        id: 'dimension',
        name: 'Dimension',
        properties: ['width', 'max-width', 'height'],
      },
      {
        id: 'decorations',
        name: 'Decorations',
        properties: ['background-color', 'border-radius', 'border', 'box-shadow'],
      },
    ],
  },
});
Sectors are the collapsible groups in the right-hand panel. Naming them explicitly is how you turn "all of CSS" into a short, deliberate set of choices.

Structure and styling are different questions

Component structure

What exists, what contains what, what may be added or removed. Controlled with the component type, droppable, draggable and removable.

Component styling

What a component may look like. Controlled with the Style Manager configuration and with stylable / unstylable on the component itself.

restricted-heading.js
// Structure and styling are separate concerns. A component can accept
// children while refusing to be restyled beyond a fixed allowance.
editor.Components.addType('brand-heading', {
  extend: 'text',
  model: {
    defaults: {
      name: 'Brand heading',
      // Only these properties reach the Style manager for this component.
      stylable: ['color', 'text-align'],
      // Everything else stays on the class in your own stylesheet.
      attributes: { class: 'brand-h2' },
    },
  },
});
A component can accept children while refusing to be restyled. This pairing — open structure, closed styling — is what a design-system editor is made of.
Beginner

6. Manage Images and Assets

The Asset Manager is the modal that opens when a reader double-clicks an image. It lists assets, accepts uploads, and hands a URL back to the selected component.

What it covers

Image uploads
Drag-and-drop or file picker, posted to an endpoint you choose.
Image URLs
Readers can paste a URL for anything you already host.
Asset selection
Double-clicking an image component opens the panel and writes the chosen src back.
Custom providers
Replace the upload entirely with uploadFile and talk to whatever storage you use.
External storage
S3, R2, Cloudinary, an in-house DAM — GrapesJS never needs to know where the bytes live, only the URL.
asset-manager.js
grapesjs.init({
  container: '#gjs',
  assetManager: {
    // Seed the panel with images you already host.
    assets: [
      'https://cdn.example.com/hero.jpg',
      { src: 'https://cdn.example.com/team.jpg', name: 'Team', category: 'People' },
    ],
    // Your upload endpoint. Set `upload: false` to disable uploading entirely.
    upload: 'https://api.example.com/uploads',
    uploadName: 'files',
    headers: { Authorization: 'Bearer <token>' },
    multiUpload: true,
    // Add the response's assets to the panel automatically. Your endpoint must
    // answer with { data: [ ...assets ] }.
    autoAdd: true,
  },
});
upload is the quickest route: point it at an endpoint that answers with { data: [ … ] } and set autoAdd. headers is where your auth token goes.
asset-upload.js
// Full control: upload wherever you like, then hand the URLs back.
grapesjs.init({
  container: '#gjs',
  assetManager: {
    async uploadFile(event) {
      const files = event.dataTransfer
        ? event.dataTransfer.files
        : event.target.files;

      const urls = await uploadToYourStorage(files); // S3, R2, Cloudinary…
      editor.AssetManager.add(urls);
    },
  },
});

// Without `upload` or `uploadFile`, dropped images are embedded as base64
// straight into the project — convenient in a demo, painful in production.
uploadFile hands you the raw files and gets out of the way. This is the version most production editors end up with, because uploads usually need signing, resizing or a tenant prefix.

One thing to decide early

With neither upload nor uploadFile configured, GrapesJS embeds dropped images into the project as base64. It works instantly, and it inflates the stored project until it becomes slow to load and expensive to move. Connect the Asset Manager to real storage before anyone starts producing content.

Intermediate

7. Save and Load GrapesJS Projects

The Storage Manager decides where the editable project goes. It is on by default and writes to localStorage — which is why an editor you built yesterday still has yesterday's canvas in it.

What it gives you

Project JSON
The whole editable document — components, styles, pages, assets — as a plain serialisable object.
Autosave
Save after a set number of edits rather than on every keystroke, with stepsBeforeSave.
Load
Fetch a project on init, or call loadProjectData() yourself whenever you like.
Save
Triggered by autosave, or by editor.store() from your own toolbar button.
Remote storage
A built-in fetch-based store: give it a load URL, a store URL, headers, and request/response adapters.
Custom storage
Register your own load/store pair for GraphQL, an offline cache, or anything remote does not cover.
storage-manager.js
grapesjs.init({
  container: '#gjs',
  storageManager: {
    type: 'remote',
    autosave: true,
    autoload: true,
    // Batch changes: save after N edits rather than after every keystroke.
    stepsBeforeSave: 5,
    options: {
      remote: {
        urlLoad: '/api/projects/42',
        urlStore: '/api/projects/42',
        headers: { 'X-CSRF-Token': csrfToken },
        credentials: 'include',
        // Shape the request body to match your API…
        onStore: (data) => ({ project: data }),
        // …and pull the project back out of your response.
        onLoad: (result) => result.project,
      },
    },
  },
});
onStore and onLoad are the two hooks that matter in a real API: they let the editor's payload and your endpoint's contract differ without either side compromising.
custom-storage.js
// When `remote` does not fit — GraphQL, a queue, an offline-first cache —
// register a storage of your own and select it by name.
editor.Storage.add('my-api', {
  async load() {
    const res = await fetch('/api/projects/42');
    const { project } = await res.json();
    return project; // the object you previously stored
  },
  async store(data) {
    await fetch('/api/projects/42', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ project: data }),
    });
  },
});

// storageManager: { type: 'my-api' }

// You can also drive it by hand, with no storage configured at all:
const project = editor.getProjectData();   // plain JSON — store it anywhere
editor.loadProjectData(project);           // and put it back
Or skip the Storage Manager entirely. getProjectData() returns plain JSON and loadProjectData() puts it back — plenty of production editors do exactly this and drive saving from their own application state.

Where GrapesJS stops

GrapesJS provides the editor and the project data layer, but your application decides where production data is stored. Which user owns a project, which tenant it belongs to, who may open it, how many revisions you keep, when it is backed up — none of that is in the library, and none of it should be.

Intermediate

8. Export HTML and CSS

Two calls turn the canvas into a page. They are the calls the HTML and CSS buttons on the live editor above are wired to.
export.js
const html = editor.getHtml();
const css = editor.getCss();

// Two things surprise everyone on their first export:
//
// 1. getHtml() returns the canvas wrapped in <body> … </body>.
//    Strip or template around it before you save a fragment.
// 2. getCss() includes GrapesJS's own canvas reset unless you opt out:
const pageCss = editor.getCss({ avoidProtected: true });

// Export one branch instead of the whole page:
const selected = editor.getSelected();
const partial = editor.getHtml({ component: selected });

// The editable project — NOT the same thing as the exported page.
const project = editor.getProjectData();
getHtml() and getCss() read the current document. Neither touches storage, and neither is affected by whether you have configured one.

Two things that surprise everyone

getHtml() wraps in <body>

The wrapper component is exported as a body element. If you are saving a fragment, template around it or strip it — do not assume you get a bare section back.

getCss() includes the editor's reset

GrapesJS ships a small protected stylesheet for the canvas. Pass avoidProtected: true when you want only the CSS your reader actually created.

A typical publishing pipeline

  1. GrapesJS
  2. HTML + CSS
  3. Your API
  4. Storage / CMS
  5. Published page
The exact pipeline is application-specific — some products write a static file, some store a rendered document alongside the project, some render server-side from the project JSON on every request. GrapesJS's part ends at the first arrow.

Two outputs, two jobs

getHtml() and getCss() give you what visitors see. getProjectData() gives you what the author can keep editing. Store the project; regenerate the page from it. Storing only the HTML means the next edit starts from parsed markup rather than from the document the author built.

Intermediate

9. Extend GrapesJS With Plugins

Plugins are one of the main ways to extend GrapesJS beyond the core editor. A plugin is nothing more than a function that receives the editor instance — anything you can do in your own setup code, a plugin can do too.
plugins.js
import grapesjs, { usePlugin } from 'grapesjs';
import blocksBasic from 'grapesjs-blocks-basic';
import forms from 'grapesjs-plugin-forms';

grapesjs.init({
  container: '#gjs',
  plugins: [
    usePlugin(blocksBasic, { flexGrid: true }),
    usePlugin(forms, {}),
  ],
});
usePlugin() registers a plugin with its options. Older tutorials show grapesjs.plugins.add(); that API is deprecated in current versions and logs a warning telling you to use this instead.
my-plugin.js
// A plugin is just a function that receives the editor.
// Anything you can do at init you can do inside one.
export default function dividerPlugin(editor, options = {}) {
  const category = options.category ?? 'Basic';

  editor.Blocks.add('divider', {
    label: 'Divider',
    category,
    content: '<hr class="divider" />',
  });

  editor.Commands.add('clear-canvas', {
    run: (ed) => ed.Components.clear(),
  });
}

// Then: plugins: [usePlugin(dividerPlugin, { category: 'Layout' })]
Writing one is the same work as configuring the editor, moved into a file you can reuse across projects. Blocks, component types, commands, panels, traits and style sectors can all be registered from inside.

Or install one

Need functionality that is not included in your base editor? Explore plugins and extensions available through GJS.Market. Below are real listings from the catalogue, grouped by the step they belong to.

Marketplace

Plugins for the steps you just finished

Real listings from the GJS.Market catalogue, grouped by the part of the tutorial they extend. Nothing here replaces the core — each one fills a seam the core deliberately leaves open.

Advanced

10. Build Custom Components

This is where a GrapesJS editor stops being a generic HTML editor and starts being part of your product. A custom component type is a named thing your authors can place, with its own structure, its own settings and its own rules about what may be changed.

What goes into a component type

The type
A name you register with Components.addType, optionally extending a built-in type.
The model
Defaults, children, attributes, properties, and the locks — droppable, draggable, removable, stylable.
Traits
The settings your author actually sees. changeProp writes to the model instead of to an attribute.
The view
Optional. Override rendering when the canvas needs to show something other than the exported markup.
isComponent
How GrapesJS recognises your type when it parses saved HTML back into the tree.
A block
The palette entry that creates it, with content: { type: 'your-type' }.

A product card, as a component type

  • Product Card, depth 0
  • Image, depth 1
  • Product Name, depth 1
  • Price, depth 1
  • CTA, depth 1
The author edits the image, the name, the price and the button. They cannot delete the price, reorder the parts or turn the card into something else — because droppable is false and the children are fixed.
product-card.js
editor.Components.addType('product-card', {
  // Lets GrapesJS recognise the type when parsing saved HTML.
  isComponent: (el) => el.dataset?.gjsType === 'product-card',

  model: {
    defaults: {
      name: 'Product card',
      attributes: { 'data-gjs-type': 'product-card', class: 'product-card' },

      // Author-visible settings. `changeProp` writes to the model
      // instead of to an HTML attribute.
      traits: [
        { name: 'sku', label: 'SKU', changeProp: true },
        {
          type: 'checkbox',
          name: 'showPrice',
          label: 'Show price',
          changeProp: true,
        },
      ],
      sku: '',
      showPrice: true,

      // Fixed structure: the author edits the parts, not the layout.
      components: [
        { type: 'image', attributes: { class: 'product-card__image' } },
        { type: 'text', name: 'Name', components: 'Product name' },
        { type: 'text', name: 'Price', attributes: { class: 'product-card__price' }, components: '$0.00' },
        { type: 'cta-button', components: 'Add to cart' },
      ],

      // Locks that make the card a card and not a free-form div.
      droppable: false,
      stylable: ['background-color', 'border-radius', 'box-shadow'],
    },

    init() {
      this.on('change:showPrice', this.togglePrice);
    },

    togglePrice() {
      const price = this.components().at(2);
      price?.addStyle({ display: this.get('showPrice') ? 'block' : 'none' });
    },
  },
});
The locks at the bottom are the interesting half. droppable: false stops the card becoming a container; stylable limits restyling to three properties; the traits give the author exactly two decisions.

Why this matters for a product

A SaaS application can expose product-specific components — a pricing table wired to real plans, a product card bound to a SKU, a booking widget — rather than a generic unrestricted HTML editor. Authors get fewer choices and better results, and your support queue never sees a page someone broke with a stray float.

Putting it together

Build a Controlled Editor With Your Design System

Steps 3, 5 and 10 combine into the most useful thing you can do with GrapesJS: replace "anything is possible" with "these seven things, done properly". Every mechanism is one you have already met.

Custom blocks

The palette is the menu. If it is not on the shelf, nobody can add it.

Custom components

Fixed structure per block, with the parts that should be editable marked editable.

Traits

The settings an author gets — a heading, a link, a variant — instead of raw markup.

Style Manager config

Sectors listing only the properties your system actually allows.

Allowed styles

stylable and unstylable per component, so a card can change colour but not become a float.

Reusable components

Shared pieces that stay in sync rather than being copied per page.

Templates

A starting document per page type, so nobody begins with a blank canvas.

Custom UI

GrapesJS's panels are replaceable. A product editor rarely looks like the default one.

Your SaaS design system

  • Hero
  • Feature Grid
  • Pricing
  • Testimonials
  • FAQ
  • CTA
  • Footer
Seven blocks, each backed by a component type you control. An author picks from this shelf and cannot produce a page that is off-brand, because there is nothing off-brand to pick.

Turn GrapesJS from a generic editor into an editor designed specifically for your product.

Two guides go further on this, in two directions:

Integration

Using GrapesJS With Your Framework

GrapesJS renders into a plain DOM element, so "integrating it with a framework" comes down to one question: which lifecycle hook calls init(), and which one calls destroy(). The pattern below is React; the dedicated guides cover the rest, including the parts that are genuinely framework-specific.

Editor.tsx
import { useEffect, useRef } from 'react';
import grapesjs, { type Editor } from 'grapesjs';

export function GjsEditor() {
  const ref = useRef<HTMLDivElement>(null);
  const editorRef = useRef<Editor | null>(null);

  useEffect(() => {
    if (!ref.current) return;
    editorRef.current = grapesjs.init({
      container: ref.current,
      height: '100vh',
      storageManager: false,
    });
    // GrapesJS owns this node now — React must never render into it again.
    return () => {
      editorRef.current?.destroy();
      editorRef.current = null;
    };
  }, []);

  return <div ref={ref} />;
}
The two rules that survive every framework: initialise once, and never let the framework re-render into the container afterwards. GrapesJS owns that node.

One caveat that catches everyone on a server-rendered framework: GrapesJS touches window when the module loads, so the editor has to be imported on the client only. The Next.js guide covers exactly that.

Architecture

From Tutorial to Production

A production editor usually needs more than grapesjs.init(). Not because the library is incomplete — because an editor is one layer of a product, and the layers around it are yours.

A typical production stack

  1. Your application
  2. Authentication
  3. GrapesJS editor
  4. Custom components / blocks
  5. Storage API
  6. Database / CMS
  7. Asset storage
  8. Publishing
GrapesJS occupies one band of this diagram. Everything above and below it is application code you write, buy or already have.

What the surrounding layers have to answer

Authentication
Who is editing, and how does the editor's storage call prove it?
Permissions
Who may edit which page, and who may publish it?
Project ownership
Which user or team a project belongs to, and what happens when they leave.
Multi-tenancy
Keeping one customer's projects, assets and templates away from another's.
Autosave
How often, what happens on a failed save, and what the reader sees when it fails.
Templates
Where a new page starts from, and how a template change reaches pages already created.
Asset storage
Where uploads go, how they are named, and who is allowed to fetch them.
Publishing
How the exported HTML and CSS become a page a visitor can load.
Error handling
A save that fails silently is the worst bug an editor can have.
Backups
Project JSON is small and compresses well. There is no excuse for losing it.
Security
Custom code blocks and pasted HTML are user input. Sanitise on the way out.
Performance
Large projects, large asset lists and long undo stacks all have a cost worth measuring.
Versioning
Revisions, drafts, and the ability to roll back a page someone broke.
The dividing line

GrapesJS responsibilities vs your application's

Nothing below is a criticism of GrapesJS — it is an editor framework, and this is the correct place for it to stop. Knowing the line before you start is what keeps a three-week build from becoming a nine-month one.

Your application
  • AuthenticationNot in the library, by design.
  • Roles and permissionsWho may edit, who may publish.
  • Project ownershipUsers, teams, transfer, deletion.
  • Multi-tenancyIsolation between customers.
  • VersioningDrafts, revisions, rollback.
  • PublishingTurning an export into a live page.
  • BackupsRetention and restore.
  • Hosting and domainsDNS, certificates, delivery.
GrapesJS core
  • Editing canvasThe iframe, selection, hover, toolbars.
  • Drag and dropMoving, nesting and reordering components.
  • Component treeTypes, children, attributes, traits.
  • Style ManagerWriting CSS rules for the selection.
  • Responsive editingDevices and per-breakpoint styles.
  • Undo and redoCore commands, keyboard bound by default.
  • Project dataSerialising and restoring the document.
  • HTML/CSS exportgetHtml() and getCss().
A plugin or your setup code
  • Block libraryThe core ships none — a preset, a plugin, or yours.
  • Rich text editingA minimal RTE ships; CKEditor/TinyMCE/Froala swap in.
  • Asset pipelineThe panel ships; the storage behind it does not.
  • Storage adapterlocal and remote ship; your API is yours.

Eight of these are yours outright. That is the honest shape of the work, and it is the same shape whichever visual editor you pick.

Pick your path

What Are You Building?

The core is the same for all of these. What differs is the layer around it — and each of these has a guide of its own.

Avoid these

Common GrapesJS Mistakes

Every one of these comes from the same place: treating the editor as the product rather than as one layer of it.

  1. Confusing Blocks with Components

    You end up adding behaviour to a palette entry and wondering why it does nothing once the item is on the canvas.

    Do this instead

    A Block only creates things. All behaviour — traits, locks, rendering, validation — belongs to the Component type it creates.

  2. Storing everything in browser state

    The default storage writes to localStorage. It looks like saving right up until a reader switches device, clears their browser, or opens the same project in two tabs.

    Do this instead

    Decide where projects really live before anyone produces content, and set storageManager: false until you have.

  3. Not configuring storage at all

    Work silently persists to a store you never designed, and load order becomes unpredictable once several projects exist.

    Do this instead

    Configure remote storage, register a custom one, or drive save and load yourself with getProjectData() and loadProjectData().

  4. Creating a component type for everything

    Forty near-identical types, each with its own traits, and a palette nobody can navigate.

    Do this instead

    Prefer one type with traits over five types that differ by a colour. Extend built-in types instead of rebuilding them.

  5. Leaving the Style Manager wide open

    Authors reach for float, absolute positioning and 13px margins, and every page drifts further from the design system.

    Do this instead

    List your sectors explicitly and use stylable / unstylable per component. Fewer controls produce better pages.

  6. Treating GrapesJS as a complete CMS

    Weeks lost looking for the users, roles, workflow and publishing features that were never there.

    Do this instead

    Read the responsibility split in step 12 first. GrapesJS is the editing layer; the CMS around it is your product.

  7. Not planning asset storage

    With no upload configured, images are embedded as base64 and the stored project grows until it is slow to load and awkward to move.

    Do this instead

    Wire the Asset Manager to real storage on day one, even if that storage is a folder on disk.

  8. Not defining a publishing workflow

    You have an editor that saves and no answer to "how does this become a page a visitor can open?".

    Do this instead

    Sketch the pipeline from step 8 before building the editor. It usually changes what you store.

  9. Putting every customisation into one plugin

    A single 2,000-line file that registers blocks, types, panels and commands, and cannot be reused or tested in parts.

    Do this instead

    One plugin per concern. They compose, and each one can be given options.

  10. Ignoring responsive behaviour until the end

    Pages that look right on the desktop canvas and break on a phone, with hundreds of desktop-only rules already written.

    Do this instead

    Switch devices while you build. Styles are written per device, so authoring at one width bakes that width in.

Troubleshooting

Common Problems

The five things most likely to go wrong on a first build, and what to check for each.

The editor does not appear

Usually a mounting problem rather than a GrapesJS problem.

Check

  • The container element exists in the DOM at the moment init() runs.
  • The container has a height — a zero-height element renders a zero-height editor.
  • The GrapesJS stylesheet is loaded; without it the editor is present but invisible.
  • Initialisation runs on the client, not during server rendering.

Styles are missing or the editor looks broken

Two different stylesheets are involved and it is easy to load neither.

Check

  • grapesjs/dist/css/grapes.min.css is loaded for the editor's own chrome.
  • Your page's CSS is passed to the canvas — it is an iframe, so your app's stylesheet does not reach it automatically.
  • The Style Manager has sectors configured; an empty sectors array renders an empty panel.
  • The selected component is not marked unstylable for the property you are looking for.

The project does not save

Listen for storage:error — GrapesJS reports failures rather than swallowing them.

Check

  • storageManager is configured, and type matches a storage that is actually registered.
  • urlStore is reachable and returns a success status.
  • Credentials and headers are being sent — remote storage defaults credentials to include.
  • There is no CORS error in the network panel; a blocked preflight looks exactly like a silent failure.

In React, the editor duplicates or dies on re-render

Almost always a lifecycle problem, not a GrapesJS one.

Check

  • init() runs once, in an effect with an empty dependency array.
  • destroy() runs in the cleanup — React 18 Strict Mode mounts effects twice in development.
  • React never renders children into the container element after init().

The build or the server crashes on import

GrapesJS touches window when the module is evaluated, so it cannot be imported during server rendering.

Check

  • The editor component is loaded client-side only — dynamic import with SSR disabled, or an import inside an effect.
  • The stylesheet is not imported into a server-rendered module.
Next steps

Continue Learning GrapesJS

Where to go once the editor above makes sense, roughly in order of difficulty.

Tutorial or reference?

This page is the build: install, configure, extend, ship. The complete guide is the reference — the architecture, the ecosystem and the reasoning behind the design. Most people end up reading both, in that order.

Read the complete guide
Services

Need Help Building a GrapesJS Editor?

Most of this tutorial is a day's work. The layer underneath it — storage, tenancy, publishing, an editor that matches your design system — is where projects get long. GJS.Market can take that part on.

  • Custom components
  • Custom plugins
  • SaaS page builders
  • Migrations
  • Integrations
  • White-label editors
  • Storage and API integration
  • React / Next.js / Vue / Angular integration
  • Production architecture
FAQ

Frequently Asked Questions

What is GrapesJS?

GrapesJS is an open-source web builder framework: a drag-and-drop visual editor you embed in your own application. It gives you a canvas, a component tree, a style manager, an asset manager and an export step, and leaves accounts, storage and publishing to the application around it.

Is GrapesJS free?

Yes. GrapesJS is free to download and use, including commercially. There is no licence fee, no seat count and no hosted service you have to buy — you run it yourself. Optional plugins from a marketplace may be paid; the editor itself is not.

Is GrapesJS open source?

Yes. The core is published under the BSD-3-Clause licence, and the official React wrapper @grapesjs/react under MIT. Both permit commercial use and modification. The source is on GitHub and the package is on npm.

How do I install GrapesJS?

Either npm install grapesjs in a project with a build step, or two tags from a CDN in a plain HTML file. Both give you the same library; the CDN route needs no tooling at all. Remember to load the stylesheet as well as the script — without it the editor renders but looks broken.

How do I create my first GrapesJS editor?

Add an empty element to your page, then call grapesjs.init({ container: '#gjs' }). That is genuinely all it takes. In practice you also want height, fromElement: false so the editor does not adopt existing markup, and storageManager: false until you have decided where projects will live.

What is a GrapesJS Block?

A Block is an entry in the palette a user drags from. It holds a label, a category, an icon and the content to create. It has no behaviour of its own — it only produces Components. The GrapesJS core ships no blocks at all: you write them, or you add a preset plugin.

What is a GrapesJS Component?

A Component is a node in the canvas: a model with a type, attributes, styles, children and traits. The canvas is a tree of Components, and the exported HTML is generated from that tree rather than the other way round.

How do I create a custom component?

Call editor.Components.addType('my-type', { model, view }), optionally extending a built-in type. The model holds defaults, children, traits and the locks — droppable, stylable, removable — that decide what an author may change. Add isComponent so GrapesJS recognises the type when parsing saved HTML.

How do I add custom blocks?

editor.Blocks.add('my-block', { label, category, media, content }). content takes either an HTML string or a component definition such as { type: 'my-type' }. The object form is what you use once you have your own component types.

How do I save GrapesJS projects?

Configure the Storage Manager with type: 'remote' and load/store URLs, register a custom storage with editor.Storage.add(), or skip it entirely and call getProjectData() and loadProjectData() from your own code. Storage is on by default and writes to localStorage, which is rarely what you want in production.

How do I export HTML and CSS?

editor.getHtml() and editor.getCss(). Two things to know: getHtml() returns the canvas wrapped in a body element, and getCss() includes GrapesJS's own protected canvas reset unless you pass avoidProtected: true.

Can I use GrapesJS with React?

Yes. Initialise it in an effect with an empty dependency array, destroy it in the cleanup, and never let React render into the container again. There is also an official wrapper, @grapesjs/react, if you would rather compose the editor's UI as React components.

Can I use GrapesJS with Next.js?

Yes, with one caveat: GrapesJS touches window at module scope, so it must be loaded on the client only — a dynamic import with server rendering disabled, or an import inside an effect. Everything else is the same as plain React.

Can I use GrapesJS with Vue or Angular?

Yes. GrapesJS renders into a plain DOM element, so it works with any framework: call init() in the mount hook and destroy() in the teardown hook. There is no official Vue or Angular wrapper — the integration is a few lines either way.

Can I build a SaaS page builder with GrapesJS?

Yes, and it is one of the most common reasons to choose it. GrapesJS supplies the editing layer; your application supplies accounts, permissions, tenancy, storage, templates and publishing. Knowing that split before you start is the difference between a short project and a long one.

Can I extend GrapesJS with plugins?

Yes. A plugin is a function that receives the editor instance, so it can register blocks, component types, commands, panels, traits and style sectors. Register plugins with usePlugin(); the older grapesjs.plugins.add() API is deprecated.

Where can I find GrapesJS plugins?

Officially maintained plugins live on npm under the GrapesJS organisation. GJS.Market catalogues community and commercial plugins by category — blocks, components, storages, assets, rich text editors, presets and developer tools — and every listing on this page comes from it.
Your move

Ready to Build With GrapesJS?

Start with the core editor, customize it for your product, and extend it with plugins and integrations when you need more functionality.

Start here

Start the Tutorial

Install GrapesJS and have an editor running in the next ten minutes.

Go to step 1
Extend

Explore Plugins

Blocks, components, storages and asset providers from the GJS.Market catalogue.

Browse plugins
Build with us

Build With Our Team

Custom components, storage integration and production architecture, done with you.

Talk to us

All samples on this page were checked against GrapesJS 0.23.6 on 2026-09-03.