Skip to content
EditorStackPlayground

Adding a visual editor to a Next.js application

Every editor in this dataset mounts against a live DOM, so none of them server-render. Here is the App Router pattern that works, the libraries that fit it best, and the mistakes that cost a day.

Updated

TL;DR

  • No visual editor in this dataset server-renders: they all need a live DOM, so the editor is always a client component loaded with a dynamic import and SSR disabled.
  • Puck is the smoothest Next.js path because its renderer works in server components while the editor stays client-side, which keeps public pages fast.
  • The recurring Next.js mistake is putting the editor in the same bundle as the public page; the editor belongs on its own route, code-split, behind authentication.

The one constraint that shapes everything

Every editor here mounts against a live DOM and, in the case of GrapesJS, an iframe canvas. None of them run on the server.

In the App Router that means the editor is always a client component, loaded with a dynamic import:

'use client';
import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('@/components/Editor'), { ssr: false });

This is not a limitation to work around; it is the correct architecture. The editor is an authenticated tool, not public content.

The pattern that works

Editor route (/admin/pages/[id]/edit) — client component, dynamic import, SSR disabled, behind authentication. Bundle size here is a UX consideration for a handful of internal or paying users, not a Core Web Vitals concern.

Public route (/[...slug]) — server component that fetches the stored page data and renders it. For Puck, that means importing Render rather than Puck, which keeps the editor entirely out of the public bundle.

Storage — your database. All the libraries in this dataset hand you data and step back; none of them require a vendor store.

The separation between these two routes is the whole integration. Get it wrong and you ship an editor bundle to every visitor.

Which library fits Next.js best

Puck is the smoothest fit. The editor is a client component, the renderer works in server components, and the documentation targets the App Router directly. Our benchmark measured 90.4 kB gzip for the editor with React external.

GrapesJS works well with the dynamic-import pattern and is the choice when output must be HTML or the editor must also run outside React. It is the heaviest at 294.6 kB gzip, which is fine on an editor route and unacceptable on a public one. The GrapesJS in Next.js guide has a server component that loads the project and a client component that mounts the editor, built and run in CI.

Craft.js fits the same pattern at 29.2 kB, with the editor interface as your project.

Plate and Editor.js fit the same way for document editing rather than page building.

Mistakes that cost a day each

Importing the editor at module scope in a server component. The error is a window is not defined during build, and it is the first thing everyone hits.

Putting the editor and the renderer in one module. Now the public page pulls the editor into its bundle. Split them; check with the bundle analyser rather than assuming.

Forgetting the container needs a real height. An editor in a parent with no height renders a zero-height canvas, and the report is always "nothing appears".

Re-initialising the editor on every render. Initialise once in a mount effect with an empty dependency array, destroy on unmount, and keep option objects out of the dependencies. In development React's StrictMode will mount twice, and without a proper teardown you get two editors stacked in one container.

Mirroring editor state into React state. Read from the editor instance; do not keep a parallel copy of the document in your store.

Publishing and preview

The pattern most teams land on: save draft data on autosave, publish by copying draft to published, and preview by rendering the draft through the same renderer the public route uses with a preview flag. Because the renderer is shared, preview cannot drift from production.

If you need publish-without-deploy for marketing pages specifically, that is what Builder.io and Plasmic sell, and our comparison covers whether that is worth a platform.

What comes after integration

Integration is around 40 hours in our build-versus-buy model. Asset uploads, autosave, versioning, permissions and a block library are the other 480, and none of them are Next.js-specific — they are the same on any stack, which is why the framework question is the easiest part of this project.

Frequently asked questions

Can I use GrapesJS with Next.js?
Yes, in a client component that initialises the editor in an effect, loaded with next/dynamic and ssr: false so the editor stays out of the server HTML and the initial bundle. Earlier versions of this page said GrapesJS touches window at import time; with GrapesJS 0.23.5 on Next.js 15.5.4 our build of a directly imported client component succeeded, so ssr: false is a bundle decision rather than a crash fix. Our GrapesJS Next.js guide has the tested code.
What is the best visual editor for Next.js?
Puck, for most teams: it is React-native, its renderer works in server components, and the documentation targets the App Router directly. GrapesJS if the output must be portable HTML or the editor must work outside React too.
How do I keep the editor out of my public bundle?
Put the editor on its own route and load it with a dynamic import. The public page should import only the renderer, which for Puck is a separate, much smaller export.
Does an embedded editor hurt Core Web Vitals?
Not if it is on its own authenticated route. It hurts them badly if the editor bundle is imported by a public page, which is the most common integration mistake.

Written by the EditorStack Research Team. How we test and what we refuse to publish is on the methodology page.