Angular owns the application
Everything around the canvas: who the user is, what they are allowed to do, and where the result goes.
- Routing
- Authentication
- Application UI
- Users
- Permissions
- Billing
- API layer
- Application state
PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access
Integrate the open-source GrapesJS visual editor into Angular applications to build drag-and-drop page builders, CMS editors, landing page builders, email editors, and white-label visual editing experiences.
26k+
GitHub stars
1.4M+
npm downloads / month
100+
plugins on GJS.Market
$0
licence fee
The stock GrapesJS build — the editor you get from `grapesjs.init()` with the standard preset, before any plugin is added. Drag a block from the right panel, select it, and edit its typography and spacing in the style manager.
Loads a third-party page from grapesjs.com. Nothing is requested until you click.
These are live GrapesJS builds, not Angular demos — and that is the point. GrapesJS renders into a container element it owns completely, so the canvas, blocks, style manager, responsive previews, asset picker and undo history you drive below behave identically once an Angular component hosts that container.
Angular powers your application. GrapesJS powers the visual editing experience. What comes out is HTML, CSS and a project JSON document that your own backend stores.
GrapesJS is framework-agnostic and can be integrated into Angular applications. You initialise the editor through Angular lifecycle hooks, keep the editor instance under your control, connect GrapesJS events to Angular application logic, and persist project data through your own backend.
There is no official Angular wrapper. The GrapesJS project maintains a React wrapper and no Angular equivalent, so an Angular integration means writing a component — roughly the thirty lines in the example below — or evaluating a third-party package on its own merits.
Jump to the working exampleGrapesJS is the visual editing engine. Angular is the application framework. Almost every integration problem people hit comes from expecting one of them to do the other's job, so it is worth being explicit about the line between them.
Everything around the canvas: who the user is, what they are allowed to do, and where the result goes.
Everything inside the canvas: what can be dragged, selected, styled and exported.
Angular is your application. GrapesJS is your visual editing engine.
One dependency and one stylesheet. GrapesJS ships its own CSS, and without it the editor mounts but renders as unstyled markup — which is the single most common reason a first integration looks broken.
1 — Add the dependency
npm install grapesjs2 — Register the stylesheet
// angular.json → projects.<app>.architect.build.options
{
"styles": [
"src/styles.css",
"node_modules/grapesjs/dist/css/grapes.min.css"
]
}Registering the stylesheet in angular.json applies it globally. If the editor lives on one lazy-loaded route and you would rather not pay for the CSS everywhere, import it inside the editor component's own stylesheet instead.
GrapesJS needs a real element to attach to, and it measures that element as it initialises. In ngOnInit the template has not been rendered yet, so a @ViewChild reference is still undefined and the call fails outright. ngAfterViewInit runs after Angular has created the component's view, which is the first moment the container exists. The same reasoning explains the height rule in the stylesheet below: an element with no resolved height produces an editor that initialises without error and displays nothing.
Before you write the component
GrapesJS replaces the contents of the element you hand it. Do not render Angular template content inside that element and do not bind to anything under it.
The host element needs a resolved height before initialisation. A zero-height container mounts an editor you cannot see.
Keep a single reference to the editor and destroy it in ngOnDestroy. Two live instances on one route will fight over keyboard shortcuts.
The grapesjs package ships its own TypeScript definitions, so Editor, Component and ProjectData are importable without a separate @types package.
Three files, nothing generated, nothing hidden. Copy them into an Angular application and you have a working visual editor on a route. Everything later on this page — storage, assets, custom components, plugins — is an addition to this component, not a rewrite of it.
// editor.component.ts
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
ViewChild,
} from '@angular/core';
import grapesjs, { type Editor } from 'grapesjs';
@Component({
selector: 'app-editor',
templateUrl: './editor.component.html',
styleUrl: './editor.component.css',
})
export class EditorComponent implements AfterViewInit, OnDestroy {
// Resolved by the time ngAfterViewInit runs — that is the whole reason
// initialisation lives there and not in ngOnInit.
@ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;
private editor?: Editor;
ngAfterViewInit(): void {
this.editor = grapesjs.init({
container: this.host.nativeElement,
height: '100%',
width: 'auto',
fromElement: false,
// No persistence yet. Wire your own API before shipping — see Storage.
storageManager: false,
blockManager: {
blocks: [
{
id: 'section',
label: 'Section',
category: 'Layout',
content: '<section class="section"><h2>Headline</h2></section>',
},
{
id: 'text',
label: 'Text',
category: 'Basic',
content: { type: 'text', content: 'Edit me' },
},
{ id: 'image', label: 'Image', category: 'Basic', content: { type: 'image' } },
],
},
});
}
ngOnDestroy(): void {
// GrapesJS owns DOM nodes, document listeners and an undo stack. Angular
// removes the host element; only destroy() releases the rest.
this.editor?.destroy();
this.editor = undefined;
}
}<!-- editor.component.html -->
<!-- GrapesJS takes this element over completely. Do not render Angular
content inside it — the editor owns everything below #editorHost. -->
<div class="editor-shell">
<div #editorHost class="editor-host"></div>
</div>/* editor.component.css */
.editor-shell {
display: block;
block-size: 100dvh;
}
/* GrapesJS measures its container at init. A host with no resolved height
produces an editor that mounts successfully and renders nothing. */
.editor-host {
block-size: 100%;
}That is the whole integration. The parts worth re-reading are the two lifecycle hooks: initialisation in ngAfterViewInit because the container has to exist first, and destroy() in ngOnDestroy because Angular removing the host element does not release what GrapesJS attached to the document.
The editor is the heaviest thing in most applications that contain one. Putting it behind a lazily-loaded route keeps it out of the initial bundle for every user who never opens it.
// app.routes.ts — the editor is the heaviest route in the app. Load it last.
import type { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', loadComponent: () => import('./home/home.component').then((m) => m.HomeComponent) },
{
path: 'pages/:id/edit',
loadComponent: () => import('./editor/editor.component').then((m) => m.EditorComponent),
},
];The editor's lifetime has to be bound to the component's. Four pieces do that work, and each of them fails in a recognisable way when it is missing.
Gets a reference to the container element. Query it by template reference variable, not by CSS selector, so a refactor of the markup cannot silently break the lookup.
The first hook that runs after the component's view exists. Initialise here. Initialising earlier gives you an undefined element reference.
Hold it in a private field, or in a service if a toolbar component elsewhere needs to trigger commands. Everything you do afterwards goes through this one object.
Call editor.destroy(). It removes the editor's DOM, detaches its document-level listeners and discards the undo stack. Skipping it is a leak that survives navigation.
Do not initialise GrapesJS before the editor container exists.
This is the error that accounts for most "GrapesJS does not work in Angular" reports. In ngOnInit, and in any constructor, the view has not been created — the container reference is undefined and initialisation throws. If the container is inside an @if block or a deferred view, ngAfterViewInit is not enough either: wait until that branch has actually rendered before you initialise.
GrapesJS attaches a large number of listeners to its canvas and fires them continuously while a component is being dragged or resized. In a zone-based Angular application every one of those listeners schedules a change-detection pass, even though nothing in your component tree has changed.
// editor.component.ts (excerpt)
import { Component, NgZone, inject, signal } from '@angular/core';
export class EditorComponent {
private readonly zone = inject(NgZone);
readonly selectedTag = signal('');
readonly savedAt = signal<Date | null>(null);
private mount(host: HTMLElement): void {
// The canvas fires pointer events continuously while a block is being
// dragged. Creating the editor outside the zone keeps those from
// scheduling a change-detection pass on every frame.
this.zone.runOutsideAngular(() => {
const editor = grapesjs.init({ container: host, height: '100%' });
// Re-enter only where editor state has to become Angular state.
editor.on('component:selected', (component) => {
this.zone.run(() => this.selectedTag.set(component.get('tagName') ?? ''));
});
editor.on('storage:end:store', () => {
this.zone.run(() => this.savedAt.set(new Date()));
});
this.editor = editor;
});
}
}
// Zoneless applications (provideZonelessChangeDetection) have no zone to
// leave. Set signals from the same editor events and drop NgZone entirely.Leave the zone once, at initialisation. Re-enter narrowly, at the specific events your interface listens to. Wrapping every editor callback in NgZone.run() gives back exactly the change-detection churn you left the zone to avoid.
The editor runs in the browser. GrapesJS builds its canvas from real DOM — it measures elements, creates an iframe and attaches document listeners — so initialisation has to happen in a browser context and must not run during a server render. Routes that merely display published output are unaffected: that is plain HTML and CSS and renders on the server like anything else.
// editor.component.ts — browser-only initialisation
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
PLATFORM_ID,
ViewChild,
inject,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import type { Editor } from 'grapesjs';
export class EditorComponent implements AfterViewInit, OnDestroy {
@ViewChild('editorHost') private host!: ElementRef<HTMLDivElement>;
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
private editor?: Editor;
async ngAfterViewInit(): Promise<void> {
if (!this.isBrowser) return;
// A dynamic import keeps the editor out of the server bundle and off the
// critical path of every route that never opens it.
const { default: grapesjs } = await import('grapesjs');
this.editor = grapesjs.init({ container: this.host.nativeElement });
}
ngOnDestroy(): void {
this.editor?.destroy();
}
}Recent Angular versions offer a render hook whose callbacks run on browser platforms only and never on the server. Where it is available it expresses the same guarantee without a platform check, and it pairs naturally with signal-based view queries.
// The same guard, expressed with the render hook instead of a platform check.
import { Component, ElementRef, afterNextRender, viewChild } from '@angular/core';
import type { Editor } from 'grapesjs';
@Component({ selector: 'app-editor', template: '<div #editorHost></div>' })
export class EditorComponent {
private readonly host = viewChild.required<ElementRef<HTMLDivElement>>('editorHost');
private editor?: Editor;
constructor() {
// afterNextRender callbacks run on browser platforms only — they never
// execute on the server, so no isPlatformBrowser check is needed.
afterNextRender(async () => {
const { default: grapesjs } = await import('grapesjs');
this.editor = grapesjs.init({ container: this.host().nativeElement });
});
}
}Check which of these your Angular version supports before choosing. Both patterns exist to enforce the same rule — the editor initialises in the browser and nowhere else. angular.dev
A page builder is not one feature. It is nine subsystems that have to agree with each other, and GrapesJS ships all nine. This is the overview; the product-level decisions — pricing, roles, templates, publishing workflow — belong to the dedicated guide linked underneath.
The editable surface, rendered in its own iframe so page styles cannot leak into your admin UI.
The typed model behind every element: what it is, what it accepts, and which settings it exposes.
The palette users drag from. A block is a named piece of content mapped to a component type.
The CSS editing panel, configurable down to which properties a given user is allowed to change.
Image and media picking. The upload endpoint behind it is yours, so plugins here mostly connect it to a provider.
Load and save. GrapesJS defines the contract and calls your API — the persistence itself is your application's.
Named, callable actions — undo, preview, export, and anything you register yourself for toolbar buttons.
The responsive breakpoints the canvas can be previewed and styled at.
The editor's own chrome — buttons, toolbars and regions, all replaceable if the editor sits inside an existing shell.
Eight of the nine ship with the editor or with a plugin. The one that is unavoidably yours is storage — which is also the one that has to know about your users, your permissions and your database.
Build a complete Angular page builderIf the editor has to be reachable from elsewhere in the application — a toolbar in the shell, a page list, a preview pane — move initialisation into an injectable service and let components talk to that instead of to the editor directly.
The canvas is a real iframe. That is deliberate: page styles cannot leak into your admin interface and your application styles cannot leak into the page being edited, which is exactly the isolation you want when the two are designed by different people.
Three artefacts, and mixing them up causes real data loss. Project data is the editable document; HTML and CSS are the published result. They are not interchangeable, because the export is lossy by design.
A JSON document holding components, styles, pages and assets. This is what you store so an editor can reopen the page exactly as it was left. Treat it as the source of truth.
The markup for the page as it will be served. Store it alongside the project data so publishing never has to boot an editor, and so a page can render even if the editor is unavailable.
The styles the editor produced for that page, scoped to the rules it manages. Serve it with the HTML — the two are one output split across two files.
// Three outputs, three different jobs. Do not use one where you need another.
const projectData = editor.getProjectData(); // editable state → store this
const html = editor.getHtml(); // markup → publish this
const css = editor.getCss(); // styles → publish this
// Reopening a saved project restores components, styles, pages and assets.
// Re-parsing exported HTML does not — it loses everything not in the markup.
editor.loadProjectData(projectData);Reparsing exported HTML back into the editor is not a substitute for project data: it loses component types, traits, page structure and anything else that was not expressible in the markup. GrapesJS is an editor, not a CMS or a backend — where the artefacts go, and what happens to them afterwards, is your application's decision.
// project-storage.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { Editor, ProjectData } from 'grapesjs';
@Injectable({ providedIn: 'root' })
export class ProjectStorage {
private readonly http = inject(HttpClient);
/** Registers a named storage the editor can then autosave into. */
register(editor: Editor, projectId: string): void {
editor.Storage.add('gjs-api', {
load: () =>
firstValueFrom(this.http.get<ProjectData>(`/api/projects/${projectId}`)),
store: async (data) => {
await firstValueFrom(
this.http.put(`/api/projects/${projectId}`, {
project: data, // what the editor reopens
html: editor.getHtml(), // what the site renders
css: editor.getCss(),
})
);
},
});
}
}// Point the editor at the storage you just registered.
this.storage.register(editor, projectId);
grapesjs.init({
container: host,
storageManager: {
type: 'gjs-api', // the name passed to Storage.add()
autosave: true,
autoload: true,
stepsBeforeSave: 5, // batch edits instead of a request per keystroke
},
});The editor never talks to your database. It calls the two methods you registered, which call HttpClient, which calls your API under the same interceptors and auth as every other request in the application.
Store the editable document
Persist getProjectData() output as the record a user reopens. Store the exported HTML and CSS in the same write so publishing has no second round trip.
Batch writes, do not stream them
autosave with stepsBeforeSave turns a burst of edits into one request. Tune the step count to your API, not to the demo default.
Autoload, or load explicitly
autoload lets the editor fetch on start. Loading explicitly instead gives you a place to show your own spinner and to handle a 403 as an application error rather than an editor one.
Keep history server-side
The undo stack lives in the browser and dies with the tab. If users need to restore yesterday's page, that is a versions table in your database, written on store.
Separate saving from publishing
A save updates the draft. Publishing promotes a specific version to the live route — a distinct endpoint, with its own permission check.
The asset manager is a picker, not a file service. Point it at an endpoint in your Angular application and it will POST the files there; everything after that — validation, storage, resizing, delivery — is yours to implement, which is what makes it possible to reuse the media library you already have.
// Assets go to your endpoint, under your auth — GrapesJS only POSTs files.
grapesjs.init({
container: host,
assetManager: {
upload: `/api/projects/${projectId}/assets`,
uploadName: 'files',
multiUpload: true,
credentials: 'include',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
// Off by default is the safer choice: base64 images end up inside saved
// markup and make every page row grow without limit.
embedAsBase64: false,
// Returning false aborts the upload before the request leaves the browser.
beforeUpload: (files: File[]) =>
files.every((file) => file.size <= MAX_UPLOAD_BYTES && ALLOWED_TYPES.has(file.type)),
},
});
editor.on('asset:upload:start', () => this.uploading.set(true));
editor.on('asset:upload:end', () => this.uploading.set(false));
editor.on('asset:upload:error', (error) => this.uploadError.set(String(error)));None of this prescribes a particular backend. S3, Cloudinary, a Laravel disk or a folder on a server all satisfy the same contract: accept a file, return a URL.
Registering a component type is how the editor learns about your product. A custom type gets its own settings panel, its own drag-and-drop rules, and a block in the palette — so content teams work with concepts from your domain instead of raw divs.
// pricing-table.type.ts — an application-specific building block.
import type { Editor } from 'grapesjs';
export function registerPricingTable(editor: Editor): void {
editor.Components.addType('pricing-table', {
// Lets the editor recognise the type again when a saved page is reparsed.
isComponent: (el) => el.classList?.contains('pricing-table'),
model: {
defaults: {
tagName: 'section',
attributes: { class: 'pricing-table' },
droppable: false,
// Traits become the right-hand settings panel for this component.
traits: [
{ type: 'text', name: 'plan', label: 'Plan name' },
{ type: 'number', name: 'price', label: 'Price' },
{ type: 'checkbox', name: 'featured', label: 'Highlight' },
],
components: `
<h3 data-gjs-type="text">Team</h3>
<p data-gjs-type="text">$29 / month</p>
`,
},
},
});
// A block is how the component reaches the user: one entry in the palette.
editor.Blocks.add('pricing-table', {
label: 'Pricing table',
category: 'Commerce',
content: { type: 'pricing-table' },
});
}Typical application-specific components
Two things make a custom type useful. isComponent lets the editor recognise your type again when a saved page is re-parsed, and traits become the settings panel a non-technical user actually edits. Both are worth writing even for a simple component.
The same integration underpins all of these. What changes between them is which blocks you register, which styles you expose, and where the output is published.
A visual page editing experience inside your Angular application, with your own blocks, your own brand constraints and your own publishing flow.
Build a complete Angular page builderLet content teams edit pages visually instead of filing tickets. The editor becomes one route of the admin application they already use.
See the headless CMS patternReusable marketing sections and layouts, assembled by a marketing team without a deploy for every change of copy.
Landing page builder patternsBuild and manage email templates visually, with the MJML preset producing markup that survives real email clients.
GrapesJS for emailA branded editor inside your SaaS product, with the editor's own chrome replaced so it reads as your interface rather than a third-party tool.
White-label the editorGive users visual editing while your application keeps control over the HTML and CSS that ends up in the database.
Learn how to build an HTML drag-and-drop builderA plugin is a function that receives the editor instance, so nothing here is Angular-specific — anything published for GrapesJS works inside an Angular component. These are the four gaps teams hit first after the editor mounts.
What users drag. A bare editor has almost nothing in the palette, so this is the first thing every integration adds.
Browse the categoryThe starting palette — the blocks a new editor is missing on first run.
Tailwind classes in the editor, if your Angular app already ships Tailwind.
Bootstrap 5 components as draggable blocks.
Configurable header sections instead of hand-built markup.
Persistence, autosave and multi-project management — the layer around the storage contract from the section above.
Browse the categoryMultiple projects in one editor, with a project list UI.
Local persistence in the browser — useful for drafts and offline editing.
Recovers unsaved work after a crash or an accidental navigation.
Storage backed by a Directus instance instead of a bespoke endpoint.
Better inline text editing and real form components. Both are capabilities users assume are built in.
Browse the categoryReplaces the built-in rich text editor with CKEditor 5.
Replaces the built-in rich text editor with TinyMCE 6.
Extra formatting controls on the default rich text toolbar.
Form components with real inputs, validation traits and submission settings.
Media providers, export formats and the email preset, for the surfaces a builder grows into.
Browse the categoryCloudinary as the asset backend, including transformations.
Exports the page as a downloadable ZIP of HTML, CSS and assets.
MJML blocks so the same editor can produce email-safe markup.
Accessibility and SEO checks inside the editor, before a page is published.
Plugin categories
Building a visual editor is possible in Angular — it is a known amount of work, not an impossible one. The question is which parts of it you want to own for the lifetime of the product.
| Capability | Build yourself | GrapesJS |
|---|---|---|
| Canvas | Build | Included |
| Drag & drop | Build | Included |
| Components | Build | Included |
| Blocks | Build | Included |
| Styling | Build | Included |
| Assets | Build | Included |
| Storage | Build | Extensible — you supply the endpoints |
| Plugins | Build an ecosystem | Plugin architecture |
| Angular integration | Native to your app | Integrate via a component |
| Licence | Yours | BSD-3-Clause — $0 to use |
"Build" is not a criticism — every one of these is buildable. It is a statement of who maintains it afterwards.
Facts in this table re-verified 2026-09-02.
Angular gives you the application framework. GrapesJS gives you the visual editing engine.
No, and it is worth understanding why the question comes up. GrapesJS publishes an official React wrapper, so developers reasonably expect an Angular equivalent. There is not one. That leaves three approaches, and for most teams the first is the right answer.
Write the component yourself — the one on this page is roughly thirty lines. You own the lifecycle, the change-detection strategy and the upgrade path, and there is no third party between your app and the editor API.
Community packages exist and can shorten the first hour. Evaluate one the way you would any dependency: check who maintains it, which Angular and GrapesJS versions it declares, and how recently it was published.
Once several components need the editor — a toolbar, a page list, a preview pane — move initialisation into an injectable service. It is direct integration with a seam, and it keeps the editor out of your component tree.
The GrapesJS organisation maintains a React wrapper and no Angular package. Any Angular library you find is third-party, however useful — treat it as a dependency choice, not as part of GrapesJS.
A demo is one component. A product is four layers, and the editor is only one of them. This is the shape most teams converge on once the editor has real users behind it.
The shell, the router, and one lazily-loaded route that owns the editor.
Configured once, at initialisation: which components exist, what can be dragged, what can be styled.
The Angular layer between the editor and your API — and the only place authorisation decisions are made client-side.
Where the artefacts live, and where every permission is enforced for real.
Nothing in this diagram is GrapesJS-specific except the second tier. That is the point: the editor is a component in an ordinary Angular application, and the rest is the architecture you would have built anyway.
See how teams package this as a productThe editor is a large piece of software running inside your application. These are the places where that actually costs something.
Create the editor when the editing route opens, not at application start. Users who never edit should never pay for it.
A dynamic import keeps GrapesJS out of the initial bundle, which is the single largest lever on this list.
Create the editor with runOutsideAngular so canvas pointer events do not trigger change detection on every frame.
Do not hand the editor a full media library at init. Paginate through your API and load assets as the picker requests them.
Very deep or very repetitive page structures make selection, styling and undo slower. Prefer components over deeply nested wrappers.
Use stepsBeforeSave so a burst of edits becomes one request rather than a request per keystroke.
None of these come with a promised number attached. Measure your own editor route before and after — the size of each effect depends on your blocks, your pages and your API.
A visual editor turns users into authors of HTML that your application then serves. Treat everything coming out of the editor as user input, because that is exactly what it is.
Editor output can contain arbitrary markup, including scripts if custom code blocks are enabled. Sanitise server-side before it is served to anyone other than its author.
Check type and size on the server, not only in beforeUpload. Store uploads outside the web root and serve them from a separate origin.
Load and save routes are ordinary API endpoints. They need the same authentication as everything else in the application.
Hiding a publish button in the Angular UI is presentation. The check that matters is the one on the endpoint.
Stored project JSON can be tampered with. Validate it on load rather than assuming it is the document you wrote.
In a multi-tenant product, derive the project's owner from the session on the server — never from an id in the request body.
Twelve things that separate a working demo from a route real users can be given.
Dependency added and the stylesheet registered, globally or in the editor component.
Read the sectiongrapesjs.init() runs in ngAfterViewInit, against a container with a resolved height.
Read the sectioneditor.destroy() in ngOnDestroy, verified by navigating away and back without leaks.
Read the sectionGuarded so nothing runs during a server render, with a placeholder in its place.
Read the sectionA named storage registered, autosave tuned, and project JSON stored alongside HTML and CSS.
Read the sectionUploads pointed at your endpoint, base64 embedding off, validation on both sides.
Read the sectionYour product's own building blocks registered as types, with traits and palette entries.
Read the sectionBlocks, rich text and export chosen deliberately rather than accumulated.
Read the sectionWho may edit, who may publish — enforced on the server, reflected in the UI.
Read the sectionServer-side sanitisation between saving a page and serving it to visitors.
Read the sectionEditor exceptions reported to the same place as the rest of your Angular errors.
The device manager exercised on a touch device, not only in a desktop browser window.
Read the sectionStart with the open-source visual editor, connect it to your Angular application, and extend it with the plugins and integrations your product needs.
Install the package, copy the component, and have an editor running on an Angular route this afternoon.
Get StartedBlocks, storage, rich text, assets, export and email — the pieces you would otherwise be writing yourself.
Browse PluginsScope the editor as a feature of your application: roles, templates, publishing and versioning.
Start a briefBuild the application with Angular. Build the visual editor with GrapesJS. Extend it with GJS.Market plugins.