# Adding a visual editor to a Next.js application

*Source: https://www.editorstack.cc/use-cases/visual-editor-for-nextjs — last updated 2026-09-17. Licensed CC BY 4.0 with attribution to EditorStack.*

## Summary

- 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:

```jsx
'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](/libraries/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](/libraries/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](/guides/grapesjs-nextjs) has a server component that loads the project
and a client component that mounts the editor, built and run in CI.

**[Craft.js](/libraries/craftjs)** fits the same pattern at 29.2 kB, with the editor interface as your
project.

**[Plate](/libraries/plate)** and **[Editor.js](/libraries/editorjs)** 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](/libraries/builder-io) and [Plasmic](/libraries/plasmic) sell, and
[our comparison](/compare/puck-vs-builder-io) covers whether that is worth a platform.

## What comes after integration

Integration is around 40 hours in our
[build-versus-buy model](/use-cases/build-vs-buy-visual-editor). 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.

