AdminKitDocs
Plugins

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.

Pro

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.

src/app/(dashboard)/forms-editors/page.tsx
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

PropTypeDefaultDescription
theme"snow" | "bubble""snow"A toolbar above the editor, or a tooltip on selected text
toolbarReactNodeThe toolbar markup: ql-formats groups of ql-* buttons and selects
modulesRecord<string, unknown>Quill modules
placeholderstringText shown while the editor is empty
defaultValuestringThe initial content, as HTML; also in the server HTML
readOnlybooleanShows the content without editing
onChange(html: string, quill: Quill) => voidCalled 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:

src/components/article-form.tsx
"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.

Last updated on

On this page