Next.js Quill Editor
A Next.js Quill editor for the App Router and React 19: plain Quill 2 in a client component that server pages can render, with snow and bubble themes.
This component is only available in AdminKit PRO. Learn more.
When people write more than a line, such as a reply or an article, they want bold text, lists and links. QuillEditor gives AdminKit for Next.js a rich text editor for that, built on plain Quill 2 (quill 2.0.3). You get the text back as HTML.
AdminKit wraps Quill itself instead of using react-quill. react-quill calls findDOMNode, an API that React 19 removed, and Next.js 16 runs on React 19.
Quill needs the browser, so QuillEditor is a client component, a file that starts with "use client". It does not need next/dynamic, though. On the server, and in the HTML files of the static export, it renders an empty editor with Quill's classes and the placeholder. So the page already has its final layout. In the browser, an effect loads Quill with import("quill") and turns that markup into the editor. Quill never runs on the server.
Example
Pages in the App Router are server components, and they can only pass plain values to a client component. Without onChange, the props are strings and JSX, so a page can render the editor directly, as the editors demo page does. Separate the ql-formats groups with a space to keep the gaps of the toolbar.
import { QuillEditor } from "@/components/plugins/quill-editor";
import { pageMetadata } from "@/lib/site";
export const metadata = pageMetadata("/forms-editors");
export default function Page() {
return (
<QuillEditor
theme="snow"
placeholder="Write a reply"
toolbar={
<>
<span className="ql-formats">
<button type="button" className="ql-bold" aria-label="Bold" />
<button type="button" className="ql-italic" aria-label="Italic" />
<button type="button" className="ql-underline" aria-label="Underline" />
</span>{" "}
<span className="ql-formats">
<button type="button" className="ql-list" value="ordered" aria-label="Numbered list" />
<button type="button" className="ql-list" value="bullet" aria-label="Bulleted list" />
</span>{" "}
<span className="ql-formats">
<button type="button" className="ql-link" aria-label="Link" />
<button type="button" className="ql-clean" aria-label="Clear formatting" />
</span>
</>
}
/>
);
}Leave toolbar out and Quill builds the theme's default toolbar, or the one in modules.toolbar.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
theme | "snow" | "bubble" | "snow" | A toolbar above the editor, or a tooltip on selected text |
toolbar | ReactNode | The toolbar markup: ql-formats groups of ql-* buttons and selects | |
modules | Record<string, unknown> | Quill modules | |
placeholder | string | Text shown while the editor is empty | |
defaultValue | string | The initial content, as HTML; also in the server HTML | |
readOnly | boolean | Shows the content without editing | |
onChange | (html: string, quill: Quill) => void | Called after every change, with the HTML and the Quill instance |
Quill reads theme, modules, placeholder, defaultValue and readOnly once, when it starts. className and other props go to .ql-container. The snow editor area is at least 15rem high. The component is in src/components/plugins/quill-editor.tsx.
Bubble theme
The bubble theme keeps the editor clean until the user selects text. With theme="bubble", the toolbar shows in a tooltip over the selection. The component keeps the toolbar markup hidden until Quill has moved it there, so it does not flash in the server HTML.
<QuillEditor
theme="bubble"
defaultValue="<p>Select this text to format it.</p>"
toolbar={
<span className="ql-formats">
<button type="button" className="ql-bold" aria-label="Bold" />
<button type="button" className="ql-italic" aria-label="Italic" />
<button type="button" className="ql-link" aria-label="Link" />
</span>
}
/>In a form
To save what the user wrote, keep the HTML from onChange in state. onChange is a function, so that form is a client component. The app builds to static files, so there is no server to run Server Actions. Send the HTML to your API with fetch instead:
"use client";
import { useState } from "react";
import { QuillEditor } from "@/components/plugins/quill-editor";
export function ArticleForm() {
const [body, setBody] = useState("<p>Draft</p>");
return (
<form
onSubmit={async (event) => {
event.preventDefault();
await fetch("/api/articles", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body }),
});
}}
>
<QuillEditor theme="snow" defaultValue={body} onChange={(html) => setBody(html)} />
<button type="submit" className="btn btn-primary mt-3">Save</button>
</form>
);
}The HTML comes from quill.getSemanticHTML(). Sanitize it on the server before other users see it.
Accessibility
- Toolbar buttons have no text. Give each one an
aria-label. - The component names the dropdowns that Quill builds from unnamed
<select>s: "Font", "Size", "Heading", "Text color", "Highlight color" and "Alignment". - Put a visible label or heading next to the editor. The placeholder is gone once the user types.
Related
- React Quill Editor: the same component in AdminKit for React (Vite)
- Bootstrap 5 rich text editor: the same plugin in the HTML version
- Form control: Bootstrap's text inputs and textareas
- Validation: validating the rest of the form
- Editors demo
Last updated on
CalendarPro
Next.js FullCalendar for the App Router: FullCalendar 7 loaded on the client with next/dynamic, with month, week and day views and drag and drop.
Vector map
A Next.js vector map for the App Router: jsVectorMap in a client component, fed marker data from a server component page, with colors that follow dark mode.