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

GrapesJS + TypeScript

GrapesJS TypeScript: Complete Integration Guide

Learn how to use GrapesJS with TypeScript, work with typed editor APIs and events, create custom components and plugins, connect storage, and integrate GrapesJS into React, Next.js, Vue or Angular applications.

Types in the packageTyped editor APIsCustom componentsPlugin developmentHTML/CSS exportReact · Vue · Angular · Next.js
The short answer

Does GrapesJS Support TypeScript?

Yes — and the types ship inside the package.

GrapesJS 0.23.6 publishes its own declaration file at dist/index.d.ts, referenced from the package's own types field. Installing grapesjs gives you the editor and its types in one dependency, so importing Editor from 'grapesjs' resolves with no extra setup and no second package.

  • Do not install @types/grapesjs. That package is not published on npm at all — the install fails with E404 rather than quietly giving you stale types.
  • There is no GrapesJS v1.x. The current release is 0.23.6, licensed BSD-3-Clause. Tutorials that talk about a 1.x line are describing a version that does not exist.
  • TypeScript 5.0 is the floor. The declaration file uses a const type parameter, so 4.9 and below cannot parse it — and report the failure as "Cannot find module 'grapesjs'", which sends most people looking for a types package they do not need.

What the types cover is the editor's API surface — the editor instance, its managers, components, blocks, events and project data. They do not describe your product's own model, and this guide is largely about keeping those two things apart.

Install it and start
The route

What You'll Learn

Twelve steps, in order. Each one links to the section that covers it, so you can start where your project actually is.

  1. Install GrapesJS with TypeScriptOne dependency, no types package, and the difference between a runtime import and a type-only import.
  2. Configure TypeScriptThe four compiler options that matter for editor work — and why strictNullChecks is the one doing real work.
  3. Type the editorgrapesjs.init returns Editor. Holding it as Editor | null is what stops the most common crash.
  4. Work with componentsComponent, ComponentDefinition, and the addType signature that most tutorials get wrong.
  5. Type blocksBlockProperties declared away from the editor, so a block is checked on its own.
  6. Handle eventseditor.on infers its callback from the event name — and stays open for your own events.
  7. Build pluginsPlugin<Options> types both parameters, and exporting the options interface is what makes it usable.
  8. Create custom componentsA composed Hero, described once as an interface and reused by editor, API and renderer.
  9. Type storage and API dataProjectData is the editor's JSON. ProjectRecord is your row. They are not the same type.
  10. Use GrapesJS with ReactThe official wrapper, and the manual useEffect version — with the cleanup StrictMode requires.
  11. Use GrapesJS with Next.jsWhere the client boundary goes, and why the editor cannot be created above it.
  12. Structure a production editorOne seam between your application and the editor, so neither leaks into the other.
Step 1

1. Install GrapesJS With TypeScript

There is one package. The types come with it, so there is no second install and no @types entry in your devDependencies.

Installbash
npm install grapesjs

The command not to run

@types/grapesjs is not deprecated, superseded or optional — it is absent from the npm registry. Running this returns a 404, and if you see it in an older tutorial that is a reliable signal the rest of that tutorial predates the bundled types too.

bash
# Don't. This package is not published — npm returns E404.
npm install --save-dev @types/grapesjs

Runtime imports vs type-only imports

grapesjs itself is a value: you call grapesjs.init(). Editor, Component and Block are types: they exist only during compilation. Marking them with import type makes that explicit, and guarantees the import is erased rather than pulled into your bundle — which matters most in the framework code, where a stray runtime import of the editor can drag it into a server render.

ts
// Runtime import: the value you actually call.
import grapesjs from 'grapesjs';

// Type-only import: erased at compile time, ships nothing to the bundle.
import type { Editor, Component, Block } from 'grapesjs';

// Editor styles. Without them the canvas renders unstyled.
import 'grapesjs/dist/css/grapes.min.css';
Next: configure the compiler
Step 2

2. Configure TypeScript

You do not need a special configuration for GrapesJS. You need four options set correctly — the rest of your tsconfig can stay whatever your project already uses.

tsconfig.jsonjson
{
  "compilerOptions": {
    // GrapesJS's bundled .d.ts uses const type parameters, a TypeScript 5.0
    // feature. On 4.9 and below the file fails to *parse*, and every import
    // from 'grapesjs' reports "Cannot find module".
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",

    // The editor manipulates real DOM nodes: container elements, iframes,
    // drag events. Without the DOM lib none of that type-checks.
    "lib": ["ES2020", "DOM", "DOM.Iterable"],

    // strict is what makes the typings worth having. In particular
    // strictNullChecks is what forces you to handle "editor not created yet",
    // which is the single most common GrapesJS runtime crash.
    "strict": true,

    "skipLibCheck": true,
    "esModuleInterop": true
  }
}
target and module
Anything from ES2020 up. The declaration file uses template literal types and const type parameters, so the constraint is the TypeScript version, not the emit target.
lib must include DOM
The editor works on real elements, iframes and drag events. Without the DOM lib, grapesjs.init({ container: element }) does not type-check and the failure looks like a GrapesJS problem rather than a config one.
strict — specifically strictNullChecks
This is the option that earns its keep. It forces you to handle the window where the editor does not exist yet: before init, after destroy, and on a ref's first render. That gap is the single most common source of runtime errors in framework integrations.
moduleResolution
bundler for Vite, Next.js and most modern setups; node16 or nodenext if you are resolving through Node's own algorithm. Both find the package's types field.
Step 3

3. Understanding GrapesJS Types

These are the names you will actually import. Each one was checked against grapesjs 0.23.6's declaration file rather than copied from the documentation site, which describes the JavaScript API and does not always use the same names.

TypeRepresentsCommon use
EditorclassThe editor instanceEverything: lifecycle, managers, export, events
EditorConfiginterfaceThe object passed to initBuilding a config away from the call site
ComponentclassOne node in the canvas treeReading and updating a selected element
ComponentDefinitioninterfaceA declared component, not an instanceChildren of a component; a block's content
ComponentPropertiesinterfaceA component's model fieldsTyping the defaults you pass to addType
AddComponentTypeOptionsinterfaceThe argument addType actually takesRegistering a custom component type
BlockclassA block in the Block ManagerThe return value of Blocks.add
BlockPropertiesinterfaceA block's declarationDeclaring blocks in their own module
TraitclassOne field in the settings panelCustom trait types and trait handlers
ProjectDatainterfaceThe editor's saved JSONStorage load and store; your database column
Plugin<T>interfaceA plugin function with typed optionsTyping a plugin you write or consume
PluginOptionstype aliasThe constraint on a plugin's optionsGeneric plugin helpers and wrappers

All of these are importable by name: import type { Editor, Component, BlockProperties } from 'grapesjs'. Only grapesjs itself needs a runtime import.

Three names you cannot import

The manager classes are declared in the file but never exported, so importing them by name fails with TS2614 — a confusing error, because the class plainly exists when you go looking for it. Index into Editor instead: the getter's return type is the same class, and the alias is stable across versions.

BlockManager
Use insteadEditor['Blocks']
StorageManager
Use insteadEditor['Storage']
ComponentManager
Use insteadEditor['Components']
Step 4

4. Type the GrapesJS Editor

grapesjs.init() returns an Editor. Annotating the variable is optional — inference already gets it right — but naming the type is what lets you pass the editor across module boundaries without widening it to any.

src/editor/createEditor.tsts
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

const editor: Editor = grapesjs.init({
  container: '#gjs',
  height: '100vh',
  storageManager: false,
});

// Both return values are typed, and they are not the same shape:
// getHtml() always returns a string, getCss() can return undefined.
const html: string = editor.getHtml();
const css: string | undefined = editor.getCss();
  • Autocomplete follows the instance: editor. lists every manager, and each manager lists its own methods with their real signatures.
  • getHtml() returns string; getCss() returns string | undefined. The difference is real, and strict mode makes you handle it.
  • destroy() is part of the API, not an extra. Framework cleanup depends on it.

Nullable references, which is where the value is

In a framework the editor does not exist for the first render, and must not exist after unmount. Typing the holder as Editor | null makes the compiler ask about both moments at every call site. Typing it as Editor and asserting it away moves that question to production.

ts
import type { Editor } from 'grapesjs';

// Not `let editor: Editor` — before init there is no editor, and the type
// should say so. Every call site is then forced to handle the empty case.
let editor: Editor | null = null;

export function exportHtml(): string {
  if (!editor) throw new Error('Editor is not initialised yet');
  return editor.getHtml(); // narrowed to Editor here
}

export function destroy(): void {
  editor?.destroy();
  editor = null;
}

Reaching the managers

Every manager hangs off the editor — editor.Blocks, editor.Components, editor.Storage, editor.Commands — and each getter is typed, so autocomplete works from the instance down. What you cannot do is import the manager classes by name; alias them through Editor instead.

ts
import type { Editor } from 'grapesjs';

// These names are NOT exported from 'grapesjs' — importing them by name is a
// compile error. Index into Editor instead and you get the same classes.
type BlockManager = Editor['Blocks'];
type StorageManager = Editor['Storage'];
type ComponentManager = Editor['Components'];

export function countBlocks(blocks: BlockManager): number {
  return blocks.getAll().length;
}
Next: react to what the editor does
Step 5

5. Working With GrapesJS Events in TypeScript

editor.on is generic over the event name, so the callback signature is derived from the string you pass. In practice that means you should annotate almost nothing — inference gives you the right parameter types, and an annotation that disagrees is a compile error rather than a silent mismatch.

src/editor/events.tsts
import type { Editor } from 'grapesjs';

export function wireEditorEvents(editor: Editor): void {
  // No annotation needed. `component` is inferred as Component and
  // `options` carries `action`, which tells add from move from clone.
  editor.on('component:add', (component, options) => {
    console.log(component.get('type'), options.action);
  });

  // A different event, a different payload — the callback signature changes
  // with the event name, so a wrong parameter list is a compile error.
  editor.on('component:selected', (component) => {
    console.log(component.getId());
  });

  editor.on('storage:end:store', () => {
    console.log('Project saved');
  });
}

The event families you will use

Component events
component:add, component:remove, component:update, component:selected, component:mount. The first argument is the Component; component:add also gets an options object whose action distinguishes an add from a move from a clone.
Editor lifecycle
load once the editor is ready, update on any change to the project, destroy on teardown. These are where you hook your own save indicator or dirty-state flag.
Storage events
storage:start:store, storage:end:store, storage:error. Useful precisely because they fire around your own storage implementation, so a failing save can surface in the UI rather than in the console.
Block events
block:drag:start, block:drag:stop and the Block Manager's own add and remove. Handy for analytics on which blocks people actually reach for.

Where inference stops

The event name is an open string union, deliberately: plugins define their own channels and those must keep compiling. The cost is that a typo in a core event name is still valid TypeScript — the callback just falls back to a loose signature and never fires. When an event handler mysteriously does nothing, check the spelling before you check the API.

ts
import type { Editor } from 'grapesjs';

export function wireCustomEvents(editor: Editor): void {
  // Your own events are allowed — the event name is a string union that stays
  // open, so plugins can define their own channels.
  editor.on('my-plugin:published', (...args: unknown[]) => {
    console.log(args);
  });

  // Which is also the trade-off: this typo compiles. The callback simply falls
  // back to (...args: any[]) and never fires.
  // editor.on('component:selcted', (component) => { ... });
}
Next: the components those events carry
Step 6

6. Type-Safe GrapesJS Components

A component type registers new behaviour in the canvas: a tag, its traits, what it accepts as children, how it is recognised when HTML is parsed back in. This is where most custom editor work happens, and where the boundary between GrapesJS's types and yours has to be drawn.

The signature to get right

editor.Components.addType(type, options) takes AddComponentTypeOptions — model, view, isComponent, extend — not a ComponentDefinition. ComponentDefinition describes a declared node inside a tree: the children of a component, or the content of a block. Tutorials that pass a ComponentDefinition to addType are quoting an older shape, and the error you get from it is not obvious.

src/editor/components.tsts
import type { Editor, Component } from 'grapesjs';

// YOUR domain model. GrapesJS knows nothing about it, and that is the point:
// this is the shape your API, your database and your React props agree on.
export interface HeroContent {
  headline: string;
  subheadline?: string;
  ctaLabel: string;
  ctaHref: string;
}

const HERO_DEFAULTS: HeroContent = {
  headline: 'Your headline',
  ctaLabel: 'Get started',
  ctaHref: '#',
};

// addType takes AddComponentTypeOptions — model / view / isComponent — not a
// ComponentDefinition. Tutorials that pass a ComponentDefinition here are
// describing an API that no longer exists.
export function registerHero(editor: Editor): void {
  editor.Components.addType('hero', {
    isComponent: (el) => el.dataset?.gjsType === 'hero',
    model: {
      defaults: {
        tagName: 'section',
        droppable: false,
        attributes: { 'data-gjs-type': 'hero' },
        traits: [
          { type: 'text', name: 'headline', label: 'Headline' },
          { type: 'text', name: 'ctaLabel', label: 'Button label' },
          { type: 'text', name: 'ctaHref', label: 'Button link' },
        ],
        ...HERO_DEFAULTS,
      },
    },
  });
}

// The bridge back to your model. component.get() is intentionally loose —
// this function is where that looseness stops and HeroContent begins.
export function readHero(component: Component): HeroContent {
  return {
    headline: component.get('headline') ?? HERO_DEFAULTS.headline,
    subheadline: component.get('subheadline'),
    ctaLabel: component.get('ctaLabel') ?? HERO_DEFAULTS.ctaLabel,
    ctaHref: component.get('ctaHref') ?? HERO_DEFAULTS.ctaHref,
  };
}
  • Traits are the settings panel. Each entry names a model field, so the panel and your interface stay in step.
  • isComponent is how a saved page is recognised on load. Without it, reloading turns your custom section back into a plain div.
  • droppable and draggable are booleans or selectors — this is where you stop people dropping a hero inside a button.

Two type systems, deliberately separate

GrapesJS types describe the editor API. Your own interfaces should describe your product's model. Mixing them feels efficient for about a week: then a field your database needs has nowhere to live except a component attribute, and a component's internals become part of your schema. Keep a function like readHero above as the only place the two meet — everything downstream takes your interface, not a Component.

Next: put it on the block shelf
Step 7

7. Type GrapesJS Blocks

A block is what appears in the left-hand shelf and what a user drags. It is not a component — it is a declaration of what to insert. Keeping the two straight is worth doing early, because their types are not interchangeable and the error message does not say so.

src/editor/blocks.tsts
import type { Editor, Block, BlockProperties } from 'grapesjs';

// BlockProperties is exported, so the block can be declared away from the
// editor and checked on its own — label, category, media, content.
const heroBlock: BlockProperties = {
  label: 'Hero',
  category: 'Sections',
  media: '<svg viewBox="0 0 24 24"><rect width="24" height="24" /></svg>',
  // A block's content can be a component definition rather than an HTML
  // string, which is how a block and a custom component type stay in sync.
  content: { type: 'hero' },
};

export function addHeroBlock(editor: Editor): Block {
  // Blocks.add(id, props) returns the created Block.
  return editor.Blocks.add('hero', heroBlock);
}

What a block declaration holds

id
First argument to Blocks.add, not a field. Unique per editor; re-adding the same id replaces the block.
label
What the user reads on the shelf. The one field here that belongs in your locale files.
category
Groups the shelf. A string, or an object when you want it collapsed by default.
content
What gets inserted: an HTML string, or a component definition. Prefer the definition — it keeps the block bound to a component type instead of to a snippet of markup.
media
The thumbnail, as inline SVG. Nothing stops you using an <img>, but inline SVG follows the editor's theme.
attributes
Applied to the shelf item itself — useful for test ids and analytics hooks, not for the inserted element.

Blocks.add returns the created Block, so you can capture it and adjust the shelf later — reordering, hiding blocks a user's plan does not include, or swapping a category label at runtime.

Next: package all of it as a plugin
Step 8

8. Build a GrapesJS Plugin With TypeScript

A GrapesJS plugin is a function that receives the editor and an options object. That is the whole contract — which is why plugins are the natural unit for anything you want to reuse across editors, ship to another team, or sell.

A shape that survives growth

One file per registration kind. The reason is not tidiness: blocks.ts exports BlockProperties objects that type-check with no editor in scope, so they can be unit-tested and reused without booting an editor at all.

my-grapesjs-plugin/ ├── src/ │ ├── index.ts # the Plugin<Options> function, and only that │ ├── types.ts # the exported Options interface │ ├── blocks.ts # BlockProperties, one per block │ ├── components.ts # editor.Components.addType calls │ └── commands.ts # editor.Commands.add calls ├── tsconfig.json ├── package.json # "types": "dist/index.d.ts" └── README.md
src/index.tsts
import type { Editor, Plugin } from 'grapesjs';

// Export the options type. A consumer cannot configure your plugin safely if
// the shape of `options` lives only inside your implementation.
export interface SectionsPluginOptions {
  category?: string;
  blockPrefix?: string;
}

// Plugin<T> is (editor: Editor, config: T) => PluginResult. Typing the
// function as Plugin<SectionsPluginOptions> checks both parameters for you.
const sectionsPlugin: Plugin<SectionsPluginOptions> = (editor, options) => {
  // Defaults belong here, not in the type — an optional field plus a
  // destructured default is what makes the call site free to omit them.
  const { category = 'Sections', blockPrefix = 'sec' } = options;

  editor.Blocks.add(`${blockPrefix}-hero`, {
    label: 'Hero',
    category,
    content: { type: 'hero' },
  });

  editor.Commands.add(`${blockPrefix}:reset`, {
    run(ed: Editor) {
      ed.setComponents('');
    },
  });
};

export default sectionsPlugin;

What the type buys you

Options
Export the interface. A consumer who cannot see the shape of your options has to read your source to configure your plugin.
Defaults
Optional fields on the type, defaults destructured in the body. Putting defaults in the type instead makes every field required at the call site.
The editor parameter
Typed for you by Plugin<T>. Everything you register inside — blocks, component types, commands — is checked against the real manager signatures.
Registration
Blocks, component types, commands and event handlers all go in the same function. A plugin that registers nothing at call time and waits for an event is fine too.

Registering it

Passing a closure keeps your options typed at the call site. The alternative — listing the plugin in plugins and its settings in pluginsOpts — types those settings as a loose record, so a misspelled key compiles and silently does nothing.

src/editor/createEditor.tsts
import grapesjs from 'grapesjs';
import sectionsPlugin, { type SectionsPluginOptions } from './my-grapesjs-plugin';

const options: SectionsPluginOptions = { category: 'Marketing' };

grapesjs.init({
  container: '#gjs',
  // Passing a closure keeps the options typed at the call site. The alternative
  // — plugins: [sectionsPlugin] with pluginsOpts — types options as
  // Record<string, any>, so a misspelled key compiles and silently does nothing.
  plugins: [(editor) => sectionsPlugin(editor, options)],
});
Next: the components a plugin ships
Step 9

9. Build Custom Components With TypeScript

A design-system section is not one component — it is a small tree with a shape your product already has a name for. Describing that shape once, as an interface, is what keeps the editor, the API and the renderer from drifting apart.

One section, four types

Hero ← one component type, one interface ├── Heading ← extends 'text' ├── Description ← extends 'text' └── Button ← extends 'link', traits: label + href
src/editor/design-system.tsts
import type { Editor, ComponentDefinition } from 'grapesjs';

// The composed shape, described once. Every layer below — the editor default,
// the API payload, the renderer — is checked against this one interface.
export interface HeroContent {
  headline: string;
  description: string;
  ctaLabel: string;
  ctaHref: string;
}

// ComponentDefinition is what goes *inside* a tree: the children of a
// component, or the `content` of a block. It is not what addType takes.
const heroChildren = (content: HeroContent): ComponentDefinition[] => [
  { type: 'text', tagName: 'h1', content: content.headline },
  { type: 'text', tagName: 'p', content: content.description },
  {
    type: 'link',
    content: content.ctaLabel,
    attributes: { href: content.ctaHref },
  },
];

export function registerDesignSystem(
  editor: Editor,
  defaults: HeroContent
): void {
  editor.Components.addType('hero', {
    model: {
      defaults: {
        tagName: 'section',
        droppable: false,
        // Children are declared, not hand-written as an HTML string, so a
        // renamed field is a compile error rather than a silently stale block.
        components: heroChildren(defaults),
        traits: [
          { type: 'text', name: 'headline', label: 'Headline' },
          { type: 'text', name: 'ctaHref', label: 'Button link' },
        ],
      },
    },
  });
}

What to type, and what not to

The content interface
Yours. Headline, description, call to action — the fields a marketer fills in and your API stores.
The component type
GrapesJS's. Registered once with addType, declaring the tag, the traits and the children.
Traits
The bridge. Each trait names a field on the model, so renaming a field in your interface should break the trait list — and with the defaults spread in, it does.
Children
ComponentDefinition objects rather than an HTML string. A string compiles no matter what you put in it; a definition is checked.
Attributes
Where data-gjs-type lives, which is what isComponent matches on reload.

The payoff is not fewer bugs today — it is that six months later, adding a field to HeroContent produces a list of every place that has to change, instead of a search across the codebase for the string 'headline'.

Next: get it in and out of your database
Step 10

10. Type GrapesJS Storage and API Data

Storage is where the two type systems meet in the most consequential way, because this is the boundary that ends up in your database. Getting it wrong is expensive later; getting it right is about twenty lines.

Two shapes, not one

ProjectData is the editor's own JSON. Its internal structure belongs to GrapesJS, changes between versions, and is not something to migrate by hand. Your ProjectRecord is a row: an id, an owner, a name, a version, timestamps — plus that opaque blob in one column. Store it, load it, and leave its insides alone.

src/types/projects.tsts
import type { ProjectData } from 'grapesjs';

// The editor's own JSON. ProjectData is deliberately open — its internal shape
// is GrapesJS's business and changes between versions, so treat it as opaque:
// store it, load it, never reach into it or migrate it by hand.

// YOUR row. This is the type your API returns and your database stores, and it
// is not a GrapesJS type. Keeping the two apart is what lets you add a column,
// change a version scheme or move providers without touching editor code.
export interface ProjectRecord {
  id: string;
  name: string;
  userId: string;
  version: number;
  updatedAt: string;
  projectData: ProjectData;
}
  1. GrapesJS
  2. Storage API
  3. Application
  4. Database

The editor produces project data. Your storage adapter is the only code that touches both sides.

src/editor/storage.tsts
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import type { ProjectRecord } from './types/projects';

export function createEditor(projectId: string): Editor {
  const editor = grapesjs.init({
    container: '#gjs',
    storageManager: {
      // The id of the storage you register below.
      type: 'remote-api',
      autosave: true,
      stepsBeforeSave: 5,
    },
  });

  editor.Storage.add('remote-api', {
    async load(): Promise<ProjectData> {
      const res = await fetch(`/api/projects/${projectId}`);
      // Throwing here is what makes the editor emit storage:error. Returning
      // an empty object instead loses the reader's work without telling them.
      if (!res.ok) throw new Error(`Load failed: ${res.status}`);
      const record = (await res.json()) as ProjectRecord;
      return record.projectData;
    },

    async store(data: ProjectData): Promise<void> {
      const res = await fetch(`/api/projects/${projectId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ projectData: data }),
      });
      if (!res.ok) throw new Error(`Save failed: ${res.status}`);
    },
  });

  editor.on('storage:error', (error) => console.error(error));

  return editor;
}

What the adapter has to handle

Load
Return the ProjectData for the current project. Throwing on a failed response is what makes the editor emit storage:error instead of silently opening an empty canvas.
Store
Send the data as-is. Do not reshape it on the way out — whatever you strip, the editor expects back on load.
Autosave
autosave with stepsBeforeSave batches changes. Every keystroke is not a request, and the number is yours to tune.
Versioning
A version column on your row, incremented server-side. Version the record, never the editor's JSON.
Multi-tenancy
The project id is a closure variable in the adapter, and ownership is checked server-side. The editor has no concept of a user and should not acquire one.
Next: put the editor inside a framework
Step 11

11. GrapesJS With React and TypeScript

There is an official React wrapper — @grapesjs/react, MIT-licensed, currently 2.0.0 — and it ships its own declaration file. It does not render React components inside the canvas; it mounts the editor and hands you the instance.

app/editor/PageEditor.tsxtsx
'use client';

import { useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor, ProjectData } from 'grapesjs';
import GjsEditor from '@grapesjs/react';
import 'grapesjs/dist/css/grapes.min.css';

interface PageEditorProps {
  projectId: string;
  onSave: (projectId: string, data: ProjectData) => void;
}

export default function PageEditor({ projectId, onSave }: PageEditorProps) {
  const editorRef = useRef<Editor | null>(null);

  return (
    <GjsEditor
      // Required. The wrapper does not import grapesjs itself — you pass the
      // module (or a CDN URL), which is what lets you control the version.
      grapesjs={grapesjs}
      options={{ height: '100vh', storageManager: false }}
      onEditor={(editor) => {
        editorRef.current = editor;
      }}
      // projectData is typed as ProjectData, so it lines up with the record
      // type your save endpoint expects.
      onUpdate={(projectData) => onSave(projectId, projectData)}
    />
  );
}

The grapesjs prop is required. The wrapper deliberately does not import the editor itself, so the version in your bundle stays the one in your package.json — and so you can point it at a CDN build instead.

Or without the wrapper

The wrapper is a convenience, not a requirement. A useEffect with a ref does the same job in about fifteen lines, and is worth understanding even if you use the wrapper, because it makes the two rules explicit: guard the ref, and destroy on cleanup.

app/editor/PageEditor.tsxtsx
'use client';

import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

export default function PageEditor() {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const editorRef = useRef<Editor | null>(null);

  useEffect(() => {
    // strictNullChecks forces this guard, and it is not ceremony: the ref is
    // null on the first render, before React has attached the div.
    if (!containerRef.current) return;

    const editor = grapesjs.init({
      container: containerRef.current,
      height: '100vh',
      storageManager: false,
    });
    editorRef.current = editor;

    // Without destroy(), React 18's development StrictMode double-mount leaves
    // two editors bound to one container.
    return () => {
      editor.destroy();
      editorRef.current = null;
    };
  }, []);

  return <div ref={containerRef} />;
}
  • The container ref is null on first render. strictNullChecks makes you say what happens then.
  • Return a cleanup that calls destroy(). React 18's development StrictMode mounts twice, and without it you get two editors in one div.
  • The wrapper's peer range is React ^18.0.0 || ^19.0.0. On React 17 you are on the manual path.
For a complete React integration guide, see GrapesJS + React. Next: the same thing with a server boundary
Step 12

12. GrapesJS With Next.js and TypeScript

The whole Next.js question is one boundary. grapesjs.init needs a real DOM element, plus document and window; a Server Component has none of them. So the editor goes in a Client Component, and everything above it can stay on the server.

Where the line goes

'use client' at the top of the component that creates the editor, and nowhere higher. The page above it stays a Server Component: it awaits params, checks the session, loads the project and passes down plain props. That split is worth guarding, because moving 'use client' up one file quietly turns your data loading into client code.

app/editor/[id]/editor-client.tsxtsx
// app/editor/[id]/editor-client.tsx
'use client';

import { useEffect, useRef } from 'react';
import grapesjs from 'grapesjs';
import type { Editor } from 'grapesjs';
import 'grapesjs/dist/css/grapes.min.css';

export default function EditorClient({ projectId }: { projectId: string }) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const editorRef = useRef<Editor | null>(null);

  // grapesjs.init needs a real element, document and window. Calling it in a
  // module body — or in a Server Component — runs it during the server render,
  // where none of those exist. useEffect only runs in the browser, which is
  // the whole requirement.
  useEffect(() => {
    if (!containerRef.current) return;
    const editor = grapesjs.init({
      container: containerRef.current,
      height: '100vh',
    });
    editorRef.current = editor;
    return () => {
      editor.destroy();
      editorRef.current = null;
    };
  }, [projectId]);

  return <div ref={containerRef} />;
}
app/editor/[id]/page.tsxtsx
// app/editor/[id]/page.tsx  — a Server Component, no 'use client'
import EditorClient from './editor-client';

interface PageProps {
  params: Promise<{ id: string }>;
}

export default async function EditorPage({ params }: PageProps) {
  const { id } = await params;

  // Auth, data loading and permissions stay on the server, fully typed.
  // Only the editor itself crosses into the client.
  return <EditorClient projectId={id} />;
}
useEffect, not the module body
Importing grapesjs on the server is harmless — it is calling init() that fails. useEffect only runs in the browser, which is the entire requirement.
Dynamic import is optional
next/dynamic with ssr: false is a bundle-size decision, not a correctness one, and it is not available inside a Server Component. Reach for it if the editor is a small part of a large page; skip it on a route that is only the editor.
CSS
Import grapesjs/dist/css/grapes.min.css from the client component. Without it the canvas renders unstyled and looks broken rather than unstyled.
Cleanup
Same as React: destroy() in the effect's return. Route transitions unmount the component, and an editor that outlives its container leaks its listeners.
For the full Next.js build, see the Next.js page builder guide. Next: Vue and Angular
Step 13

13. GrapesJS With Vue and Angular

Both work, and both follow the same shape as the manual React version: a template ref, initialisation after the element exists, destroy on teardown. The framework-specific mechanics have their own guides rather than a condensed version here.

In both cases the types are the same ones this page has been using: Editor, Component, ProjectData. The framework changes where init() is called, not what it returns.

Step 14

14. TypeScript Architecture for a Production GrapesJS Editor

Everything above fits in one file. It stops fitting at about the third custom component, and the thing that decides whether the next year is pleasant is where you put the seam between your application and the editor.

A structure that holds up

src/ ├── editor/ # everything that touches the Editor instance │ ├── createEditor.ts # grapesjs.init, one place, returns Editor │ ├── plugins.ts # Plugin<T> registrations │ ├── components.ts # Components.addType calls │ ├── blocks.ts # BlockProperties definitions │ ├── commands.ts # Commands.add calls │ └── storage.ts # Storage.add, ProjectData in and out │ ├── types/ │ ├── editor.ts # aliases over GrapesJS types you use a lot │ ├── components.ts # HeroContent and friends — YOUR model │ ├── projects.ts # ProjectRecord — your database row │ └── api.ts # request/response shapes │ └── app/ # imports from types/, never from editor/ internals

One seam, declared

Application code should not import from 'grapesjs' directly. Give it a single module that re-exports the handful of editor types your product legitimately knows about, aliases the managers that are not exported, and declares the narrow interface your UI actually depends on. Then a toolbar button takes that interface, not a whole Editor — and cannot reach into editor internals even by accident.

src/types/editor.tsts
// src/types/editor.ts — the single seam between your app and the editor.
import type { Editor, ProjectData } from 'grapesjs';

// Re-export what your application is allowed to know about.
export type { Editor, ProjectData };

// Manager classes are not exported by name; alias them here once so no other
// file has to remember that.
export type BlockManager = Editor['Blocks'];
export type StorageManager = Editor['Storage'];

// The surface your UI actually depends on. Application code takes this, not a
// full Editor, so a toolbar button cannot quietly reach into editor internals.
export interface EditorFacade {
  getHtml(): string;
  getCss(): string;
  save(): Promise<void>;
  destroy(): void;
}
Layers

The layers, and which way dependencies point

Your application on top, your types in the middle, the editor underneath. Dependencies point downward and never back up.

  1. Application

    Yours. Knows nothing about GrapesJS.

    • Routing
    • Authentication
    • Application state
    • Your UI
  2. Types

    The seam. The only place both worlds are named.

    • Domain model
    • Project records
    • API contracts
  3. Editor layer

    Yours. The only code that imports from the editor.

    • createEditor
    • Plugins
    • Component types
    • Storage adapter
  4. GrapesJS

    The editor. Typed by the package.

    • Canvas
    • Managers
    • Events
Embedding this in someone else's product?
The whole picture

15. TypeScript Architecture for a SaaS Page Builder

A page-builder product is mostly not a page builder. GrapesJS covers one box in this chain; the rest is an application you were going to write anyway, and typing the joins between them is what this guide has been building toward.

  1. SaaS application
  2. Authentication
  3. GrapesJS editor
  4. Typed components
  5. Typed plugins
  6. Storage API
  7. Database
  8. Publishing

GrapesJS owns the editing surface. Authentication, storage, tenancy, billing and publishing are yours.

The whole picture

Who owns what

GrapesJS owns the editing surface. Authentication, storage, tenancy, billing and publishing are yours.

Your product

You provide

Everything the editor has no opinion about.

  • Accounts, sessions and permissions
  • Tenancy and per-plan limits
  • The database and its migrations
  • Publishing, domains and hosting
  • Billing and usage
GrapesJS

GrapesJS provides

Everything inside the canvas, typed by the package.

  • The canvas and drag-and-drop
  • Blocks, styles, layers, traits, assets
  • HTML and CSS output
  • Project JSON via the Storage Manager
  • Component types, plugins and commands

The editor is a component of your product, not a substitute for it.

Step 16

16. Common GrapesJS TypeScript Mistakes

Eleven failures that come up repeatedly, most of them specific to this pairing rather than to TypeScript in general. Each one is a symptom you can match against and the change that fixes it.

Installing @types/grapesjs

Symptom

npm install fails with E404, or a tutorial tells you to add it and you assume your registry is broken.

Fix

Remove it. The types are inside grapesjs itself, referenced by the package's types field. There is no separate types package and there has not been one for the current line.

Typing the editor as any

Symptom

let editor: any, usually added to silence one error during setup, and never removed.

Fix

Editor from 'grapesjs'. One any at the root propagates to every manager, every event payload and every export call — you keep the compile-time cost of TypeScript and lose all of the benefit.

Leaving plugin options untyped

Symptom

A plugin takes opts: any or Record<string, unknown>; consumers guess field names and typos do nothing.

Fix

Declare and export an options interface, then type the function as Plugin<YourOptions>. Both parameters are then checked, including at the call site.

Confusing Block and Component

Symptom

Passing a Block where a Component is expected, or trying to style a block and finding it has no styles.

Fix

A block is a shelf entry describing what to insert. A component is a node in the canvas. Blocks.add takes BlockProperties; Components.addType takes AddComponentTypeOptions.

Passing ComponentDefinition to addType

Symptom

A component type registers but behaves like a plain div — no traits, no restrictions.

Fix

addType takes AddComponentTypeOptions: model, view, isComponent, extend. ComponentDefinition describes a node inside a tree — the children of a component, or a block's content.

Assuming every event has the same payload

Symptom

A handler written for component:add is reused for component:remove and the second argument is undefined.

Fix

Payloads differ per event and the types already say so. Let inference give you the parameters instead of annotating them from another handler.

Coupling database models to editor internals

Symptom

Your ProjectRecord has columns mirroring fields inside the editor's JSON, and a GrapesJS upgrade means a migration.

Fix

Treat ProjectData as opaque. One column holds it; everything you query on — owner, name, version, timestamps — lives beside it, not inside it.

Ignoring nullable editor references

Symptom

"Cannot read properties of null" on a fast navigation, a hot reload, or the first render of a route.

Fix

Editor | null, and handle the null. Non-null assertions on a ref move the problem from your terminal to your error tracker.

Following pre-types tutorials

Symptom

Named imports that do not resolve, manager methods that do not exist, an editor version that was never published.

Fix

Check the API against the declaration file in your node_modules rather than against a blog post. There is no GrapesJS v1.x; the current release is 0.23.6.

Mixing versions across packages

Symptom

The wrapper's Editor and your Editor look identical but are structurally incompatible, and the error mentions two paths.

Fix

One grapesjs in the tree. Check with npm ls grapesjs — a nested copy under a plugin is the usual cause.

Over-broad custom interfaces

Symptom

An interface that re-declares half the editor API so you can pass it around, drifting from the real types every release.

Fix

Alias what exists — Editor['Blocks'] — and declare only the narrow facade your own UI needs. Restating the editor's API in your own types is maintenance you do not have to sign up for.

None of these are TypeScript problems. They are places where the editor's model and your product's model get confused for each other, and the type system is only what makes the confusion visible early.

Step 17

17. Troubleshooting GrapesJS TypeScript Errors

Four error shapes cover almost everything. In each case the useful move is to look at the declaration file in node_modules rather than to reach for any — the answer is in there, and any only postpones the question.

Cannot find module 'grapesjs' or its corresponding type declarations

You see

TS2307 on the import line, even though the package is clearly installed and the editor runs fine at runtime.

Check

Your TypeScript version first. Below 5.0 the declaration file cannot be parsed and the failure is reported as a missing module — a genuinely misleading message. Then moduleResolution: it must be bundler, node16 or nodenext, not classic. Adding @types/grapesjs will not help; that package does not exist.

Module 'grapesjs' has no exported member 'X'

You see

TS2614 on a name you can see in the declaration file, most often StorageManager or BlockManager.

Check

The manager classes are declared but not exported. Use the indexed-access alias — Editor['Storage'], Editor['Blocks'], Editor['Components'] — which resolves to the same class. If the name is something else, grep node_modules/grapesjs/dist/index.d.ts: if it is not there, it belongs to an older version.

Argument of type ... is not assignable to parameter

You see

An event handler, an addType call, or a block declaration that matches a tutorial exactly and still will not compile.

Check

The signature, in the declaration file. Events carry different payloads per name; addType takes AddComponentTypeOptions rather than ComponentDefinition. Hover the method in your editor — the real signature is right there, and it is usually a different shape than the article you copied from.

Two incompatible Editor types

You see

A React or Next.js build where the wrapper's Editor and yours refuse to unify, and the message names two node_modules paths.

Check

Duplicate installs. npm ls grapesjs will show the second copy, usually pulled in by a plugin with a narrow peer range. Deduplicate or align the versions; the wrapper's own peer range is ^0.22.5.

For framework-specific type errors, the React and Next.js guides go deeper than this page does.

Step 18

18. GrapesJS + TypeScript Compatibility

Exact versions, checked against the registry and against the installed declaration file. The TypeScript floor in particular was measured by compiling against each release, not inferred from a changelog.

PackageVerifiedWhat it means
grapesjs0.23.6Ships its own types at dist/index.d.ts. No separate types package exists.
typescript>= 5.0The floor. 4.9 and below cannot parse the declaration file, and report it as a missing module.
typescript7.0.2The current release, and the version this guide's samples were compiled with. Everything from 5.0 up works.
@grapesjs/react2.0.0The official React wrapper, MIT-licensed, with its own bundled types.
react^18.0.0 || ^19.0.0The wrapper's React peer range. On React 17, initialise the editor manually with useEffect.
grapesjs (peer)^0.22.5The wrapper's grapesjs peer range — wide enough that the current core satisfies it.
node>=20.9.0GrapesJS is a browser library and declares no engines field. The floor you actually hit comes from your framework; Next.js 16.3.4 requires this.

Verified 2026-09-03 against registry.npmjs.org and node_modules/grapesjs/dist/index.d.ts. Every code sample on this page was compiled against these versions under strict mode before publication.

There is no "works with every TypeScript version" here, because it is not true: 5.0 is a hard floor and the failure mode below it is confusing enough to be worth stating exactly.

Step 19

19. Extend GrapesJS With TypeScript-Compatible Plugins

Once you understand the core API, plugins can provide additional functionality without requiring you to build every feature from scratch. These are current listings on GJS.Market, grouped by which part of the typed surface each one touches.

One thing this page will not tell you: none of these listings advertises bundled TypeScript declarations, so treat type support as unverified and check the plugin's own README. What is true regardless is that the editor a plugin receives is typed by the core package — so your integration code around any plugin is checked, even when the plugin itself is plain JavaScript.

Step 20

20. Build Your Own Plugin or Use an Existing One?

The line is not about difficulty. It is about whether the behaviour is specific to your product — because that is what decides who has to maintain it in two years.

RequirementBuild yourselfUse a plugin
Behaviour specific to your businessYes — nobody else will build it
Common editor functionalityYes — already solved
Full control of the codeYesYes, for open-source plugins
Effort to first working versionHigherLower starting point
Who maintains itYour teamPlugin author, plus your integration
How far you can change itAs far as you likeDepends on the plugin

In practice most editors are both: a handful of plugins for the parts every editor needs, and your own typed component types for the parts that make the product yours.

Keep going

Continue Learning GrapesJS

Where to go next, depending on whether you are still learning the editor, wiring it into a framework, or building a product around it.

Custom development

Building a Production GrapesJS Editor?

If the architecture section is where your project actually is, the remaining work is usually integration rather than editor features. That is what we do.

  • TypeScript plugins
  • Custom component types
  • React integration
  • Next.js integration
  • Storage and API integration
  • SaaS editors
  • White-label editors
  • Custom editor UI
  • Migrations from older versions
  • Production architecture review
Talk to a GrapesJS expert
Questions

Frequently Asked Questions

Does GrapesJS support TypeScript?

Yes. GrapesJS 0.23.6 publishes a declaration file with the package and references it from its own types field, so importing types works as soon as the package is installed. The types cover the editor instance, its managers, components, blocks, traits, events and project data.

Does GrapesJS include TypeScript definitions?

Yes — at dist/index.d.ts inside the grapesjs package. You can read it directly in node_modules, which is the most reliable way to check any API question on this page against the version you actually have installed.

Do I need @types/grapesjs?

No, and you cannot install it: the package is not published on npm and the install fails with a 404. If a tutorial tells you to add it, that tutorial predates the bundled types and its other advice is probably out of date too.

How do I install GrapesJS with TypeScript?

npm install grapesjs. That is the whole installation — one dependency, types included. Then import grapesjs from 'grapesjs' for the runtime value and import type { Editor } from 'grapesjs' for the types.

How do I type the GrapesJS editor?

grapesjs.init() returns an Editor, so inference already gets it right. Where it matters is holding the instance: type it as Editor | null in a ref or a class field, because the editor genuinely does not exist before mount or after destroy, and strictNullChecks makes every call site handle that.

How do I type GrapesJS components?

Component is the canvas node. ComponentDefinition describes a declared node inside a tree — the children of a component, or a block's content. Registering a new type uses editor.Components.addType(type, options), which takes AddComponentTypeOptions: model, view, isComponent and extend.

How do I type GrapesJS blocks?

BlockProperties is exported, so a block can be declared in its own module and checked without an editor in scope. editor.Blocks.add(id, props) takes that object and returns the created Block.

How do I handle GrapesJS events with TypeScript?

editor.on is generic over the event name and derives the callback signature from it, so you should rarely annotate the parameters. Note that the event name is an open string union so plugins can define their own events — which means a typo in a core event name still compiles and simply never fires.

How do I create a TypeScript GrapesJS plugin?

A plugin is a function taking the editor and an options object. Export an options interface and type the function as Plugin<YourOptions> — both parameters are then checked, and consumers can see how to configure it without reading your source.

Can I create custom GrapesJS components with TypeScript?

Yes, and it is where the types pay off most. Describe the content shape as your own interface, register the component type with addType, and let the traits name fields on that interface — so renaming a field surfaces as a compile error rather than as a blank section in production.

Can I use GrapesJS with React and TypeScript?

Yes. @grapesjs/react 2.0.0 is the official wrapper, MIT-licensed, with its own bundled types; it requires you to pass grapesjs as a prop. Its React peer range is ^18.0.0 || ^19.0.0. On React 17, or if you prefer no wrapper, a useEffect with a ref and a destroy() cleanup does the same job.

Can I use GrapesJS with Next.js and TypeScript?

Yes. Put 'use client' on the component that creates the editor and nowhere higher, and call grapesjs.init inside useEffect — it needs a real element, document and window, none of which exist during a server render. The page above it can stay a Server Component and keep loading data on the server.

Can I use GrapesJS with Vue and TypeScript?

Yes: a template ref, onMounted to initialise, onBeforeUnmount to destroy. Keep the Editor out of ref() or reactive() — wrapping it makes Vue proxy an object that manages its own internals. There is no official Vue wrapper.

Can I use GrapesJS with Angular and TypeScript?

Yes: @ViewChild for the container, ngAfterViewInit to initialise, ngOnDestroy to tear down, and runOutsideAngular so the editor's event loop does not drive change detection. There is no official Angular wrapper; the packages on npm are third-party.

Can I connect GrapesJS to a TypeScript backend?

Yes — through the Storage Manager, by registering a storage with load and store functions that call your API. Keep the two types apart: ProjectData is the editor's JSON and should be stored opaquely, while your own record type holds the id, owner, name, version and timestamps you actually query on.

Where can I find TypeScript-compatible GrapesJS plugins?

The GJS.Market catalogue lists 100+ GrapesJS plugins. Note that individual listings do not currently advertise bundled type declarations, so check each plugin's README — but the editor object a plugin receives is typed by the core package either way, so your own integration code around it is still checked.
Start building

Build Your GrapesJS Editor With TypeScript

Start with the typed GrapesJS API, build your own components and plugins, connect your application infrastructure, and extend the editor when your product needs more functionality.

Learn

Start the tutorial

Install the package, type the editor and get a working typed setup in a few minutes.

Start the tutorial
Extend

Explore plugins

Storage adapters, component types and developer tooling already built for GrapesJS.

Explore plugins
Build

Get custom development

Typed plugins, framework integration and production architecture, built with you.

Get custom development

TypeScript does not make GrapesJS safer. It makes a customised GrapesJS editor maintainable once it outgrows one file.