AdminKitDocs
Plugins

React Quill Editor

A React Quill editor for React 19 in AdminKit PRO: Quill 2 wrapped in a QuillEditor component, with snow and bubble themes, a custom toolbar and HTML output.

Pro

This component is only available in AdminKit PRO. Learn more.

Use QuillEditor when a plain textarea is not enough: for a reply, an article or a product description with bold text, lists and links. It is a rich text editor for AdminKit for React, and it gives you the text as HTML.

The component wraps plain Quill 2 (quill 2.0.3) itself, not react-quill. That is what makes it work with React 19: react-quill calls findDOMNode, which React 19 removed.

You don't control the editor's value from state. defaultValue is the HTML it starts with, and onChange receives the HTML after every change. Quill loads with a dynamic import("quill") when the first editor appears on a page, so Vite keeps it out of the main bundle. Until Quill has loaded, the component renders the editor's markup with Quill's classes, so the card does not change height.

Example

You write the toolbar yourself, as markup in toolbar. It holds ql-formats groups of ql-* buttons and selects, as in Quill's custom toolbar. Put a space between the groups, because the spaces give the toolbar its gaps.

import { QuillEditor } from "@/components/plugins/quill-editor";

export function ReplyEditor() {
  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>
        </>
      }
    />
  );
}

Without toolbar, Quill builds the default toolbar of the theme, or the one you describe in modules.toolbar.

Props

PropTypeDefaultDescription
theme"snow" | "bubble""snow"The toolbar above the editor, or a tooltip on selected text
toolbarReactNodeThe toolbar markup
modulesRecord<string, unknown>Quill modules
placeholderstringText shown while the editor is empty
defaultValuestringThe initial content, as HTML
readOnlybooleanShows the content without editing
onChange(html: string, quill: Quill) => voidCalled after every change, with the HTML and the Quill instance

theme, modules, placeholder, defaultValue and readOnly are read once, when Quill starts. className and other props go to the .ql-container element. With the snow theme, the editor area is at least 15rem high. The component is in src/components/plugins/quill-editor.tsx.

Bubble theme

For a lighter look, such as editing text in place, use theme="bubble". It has no fixed toolbar. Instead, the toolbar appears in a tooltip when the user selects text. The component hides the toolbar markup until Quill has moved it into the tooltip.

<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

The editor is not a form field. Keep the HTML from onChange in state and send it with the form, for example in a hidden input:

import { useState } from "react";
import { QuillEditor } from "@/components/plugins/quill-editor";

export function ArticleForm({ onSave }: { onSave: (body: string) => void }) {
  const [body, setBody] = useState("<p>Draft</p>");

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        onSave(body);
      }}
    >
      <QuillEditor theme="snow" defaultValue={body} onChange={(html) => setBody(html)} />
      <input type="hidden" name="body" value={body} />
      <button type="submit" className="btn btn-primary mt-3">Save</button>
    </form>
  );
}

onChange gets Quill's semantic HTML, from quill.getSemanticHTML(). Sanitize it on the server before you show it to other users.

Accessibility

  • Quill's toolbar buttons have no text. Give each one an aria-label, as in the examples.
  • Quill builds its dropdowns from the <select>s without a name. The component labels the pickers it recognizes: "Font", "Size", "Heading", "Text color", "Highlight color" and "Alignment".
  • The editor area is a contenteditable element that Quill manages. Put a visible label or heading next to it, since the placeholder disappears once the user types.

Last updated on

On this page