Beginner
The core loop: get an editor on screen and put things in it.
PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access
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.
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.
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.
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 demoVery little. If you can write a page by hand, you can follow this.
You should recognise a tag, a class and a CSS property. GrapesJS edits HTML and CSS — nothing more exotic.
Enough to read an object literal and a function. Every example here is plain JavaScript.
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.
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.
npm install grapesjsNo 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.
<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>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.
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.
<!-- The editor takes over this element completely.
Do not render anything inside it yourself. -->
<div id="gjs"></div>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,
});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 DemoBlock
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 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' },
});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.
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 })],
});A typical subtree
// 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'));
});// 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' },
],
},
],
},
},
});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'],
},
],
},
});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.
// 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' },
},
},
});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,
},
});// 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.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.
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,
},
},
},
});// 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 backGrapesJS 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.
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() 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
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.
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, {}),
],
});// 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' })]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.
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.
Step 3 — a palette to start from instead of writing every block by hand.
Browse the categoryThe starter palette step 3 says the core is missing: a handful of basic blocks, registered for you.
A web-page preset — blocks, panels and a sensible default configuration in one plugin, instead of assembling them yourself.
Bootstrap 5 layout blocks, if your product already ships Bootstrap and the editor should produce the same markup.
A library of ready-made responsive Tailwind blocks, for teams whose design system is already Tailwind classes.
A tabs component type, complete with its block — a worked example of everything step 10 covers.
A touch-enabled slider block built on Swiper, one of the components almost every page builder eventually needs.
Form components and blocks: input, textarea, select, checkbox, radio and button, wired up as real component types.
An icon component with an Iconify picker in a modal — a good model for a trait that opens custom UI.
An IndexedDB storage wrapper: the same Storage Manager contract from step 7, backed by the browser's own database.
A Firestore storage wrapper, if you would rather point the editor at a hosted database than build a save endpoint.
Replaces the default uploader with Cloudinary — the uploadFile hook from step 6, already written.
Swaps the Asset Manager's uploader for Filestack, for teams that already use it for the rest of their uploads.
Steps 8 and 9 — richer text editing, code editing and export targets.
Browse the categorySwaps the built-in rich text editor for CKEditor 5 in inline mode. CKEditor itself is licensed separately by its vendor.
Edit the HTML and CSS of the selected component directly on the canvas — useful while you are still learning the tree.
Packages the export from step 8 into a downloadable zip, which is the quickest publishing pipeline there is.
More actions in the built-in rich text editor, without pulling in a full third-party editor.
A product card, as a component type
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' });
},
},
});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.
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
Turn GrapesJS from a generic editor into an editor designed specifically for your product.
Two guides go further on this, in two directions:
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.
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} />;
}React
Build a GrapesJS editor inside a React application.
React guideNext.js
Integrate GrapesJS with a Next.js application and client-side editor architecture.
Next.js guideVue
Add GrapesJS to a Vue application.
Vue guideAngular
Integrate GrapesJS with Angular.
Angular guideVanilla JavaScript
Start with the core GrapesJS API — no framework, no wrapper, no build step required.
Official docsOne 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.
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
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.
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.
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.
Build a visual editor for marketing pages.
Landing page builderLet customers create pages inside your application.
SaaS page builderAdd visual editing to an existing product.
Embeddable editorBuild email creation workflows.
Email builderCreate your own branded page-building experience.
White-label builderAdd visual editing to a headless content system.
Headless CMS editorEvery one of these comes from the same place: treating the editor as the product rather than as one layer of it.
You end up adding behaviour to a palette entry and wondering why it does nothing once the item is on the canvas.
A Block only creates things. All behaviour — traits, locks, rendering, validation — belongs to the Component type it creates.
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.
Decide where projects really live before anyone produces content, and set storageManager: false until you have.
Work silently persists to a store you never designed, and load order becomes unpredictable once several projects exist.
Configure remote storage, register a custom one, or drive save and load yourself with getProjectData() and loadProjectData().
Forty near-identical types, each with its own traits, and a palette nobody can navigate.
Prefer one type with traits over five types that differ by a colour. Extend built-in types instead of rebuilding them.
Authors reach for float, absolute positioning and 13px margins, and every page drifts further from the design system.
List your sectors explicitly and use stylable / unstylable per component. Fewer controls produce better pages.
Weeks lost looking for the users, roles, workflow and publishing features that were never there.
Read the responsibility split in step 12 first. GrapesJS is the editing layer; the CMS around it is your product.
With no upload configured, images are embedded as base64 and the stored project grows until it is slow to load and awkward to move.
Wire the Asset Manager to real storage on day one, even if that storage is a folder on disk.
You have an editor that saves and no answer to "how does this become a page a visitor can open?".
Sketch the pipeline from step 8 before building the editor. It usually changes what you store.
A single 2,000-line file that registers blocks, types, panels and commands, and cannot be reused or tested in parts.
One plugin per concern. They compose, and each one can be given options.
Pages that look right on the desktop canvas and break on a phone, with hundreds of desktop-only rules already written.
Switch devices while you build. Styles are written per device, so authoring at one width bakes that width in.
The five things most likely to go wrong on a first build, and what to check for each.
Usually a mounting problem rather than a GrapesJS problem.
Check
Two different stylesheets are involved and it is easy to load neither.
Check
Listen for storage:error — GrapesJS reports failures rather than swallowing them.
Check
Almost always a lifecycle problem, not a GrapesJS one.
Check
GrapesJS touches window when the module is evaluated, so it cannot be imported during server rendering.
Check
Where to go once the editor above makes sense, roughly in order of difficulty.
The core loop: get an editor on screen and put things in it.
Making the editor yours: custom types, plugins, storage and tooling.
Everything around the editor: architecture, tenancy, publishing and production.
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 guideMost 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.
Start with the core editor, customize it for your product, and extend it with plugins and integrations when you need more functionality.
Install GrapesJS and have an editor running in the next ten minutes.
Go to step 1Blocks, components, storages and asset providers from the GJS.Market catalogue.
Browse pluginsCustom components, storage integration and production architecture, done with you.
Talk to usAll samples on this page were checked against GrapesJS 0.23.6 on 2026-09-03.