# 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.

> This component is only available in **AdminKit PRO**. [Learn more](/pricing/#react).

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](/react/), and it gives you the text as HTML.

The component wraps plain [Quill](https://quilljs.com/) 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](https://quilljs.com/docs/modules/toolbar#container). Put a space between the groups, because the spaces give the toolbar its gaps.

```html
<div id="quill-example-toolbar">
  <span class="ql-formats">
    <button type="button" class="ql-bold" aria-label="Bold"></button>
    <button type="button" class="ql-italic" aria-label="Italic"></button>
    <button type="button" class="ql-underline" aria-label="Underline"></button>
  </span>
  <span class="ql-formats">
    <button type="button" class="ql-list" value="ordered" aria-label="Numbered list"></button>
    <button type="button" class="ql-list" value="bullet" aria-label="Bulleted list"></button>
  </span>
  <span class="ql-formats">
    <button type="button" class="ql-link" aria-label="Link"></button>
    <button type="button" class="ql-clean" aria-label="Clear formatting"></button>
  </span>
</div>
<div id="quill-example"></div>

<script>
new Quill("#quill-example", {
  theme: "snow",
  placeholder: "Write a reply",
  modules: { toolbar: "#quill-example-toolbar" }
});
</script>
```
```tsx code
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `theme` | `"snow" \| "bubble"` | `"snow"` | The toolbar above the editor, or a tooltip on selected text |
| `toolbar` | `ReactNode` | | The toolbar markup |
| `modules` | `Record<string, unknown>` | | [Quill modules](https://quilljs.com/docs/modules) |
| `placeholder` | `string` | | Text shown while the editor is empty |
| `defaultValue` | `string` | | The initial content, as 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 |

`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.

```html
<div id="quill-bubble-toolbar">
  <span class="ql-formats">
    <button type="button" class="ql-bold" aria-label="Bold"></button>
    <button type="button" class="ql-italic" aria-label="Italic"></button>
    <button type="button" class="ql-link" aria-label="Link"></button>
  </span>
</div>
<div id="quill-bubble"><p>Select this text to format it.</p></div>

<script>
new Quill("#quill-bubble", {
  theme: "bubble",
  modules: { toolbar: "#quill-bubble-toolbar" }
});
</script>
```
```tsx code
<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:

```tsx
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.

## Related

- [Next.js Quill Editor](/nextjs/docs/plugins/rich-text-editor/): the same component in AdminKit for Next.js
- [Bootstrap 5 rich text editor](/bootstrap/docs/plugins/rich-text-editor/): the same plugin in the HTML version
- [Form control](/react/docs/forms/form-control/): Bootstrap's text inputs and textareas
- [Validation](/react/docs/forms/validation/): validating the rest of the form
- [Editors demo](https://react-demo.adminkit.io/forms-editors)

---

Canonical HTML page: https://adminkit.io/react/docs/plugins/rich-text-editor/

Site overview for agents: https://adminkit.io/llms.txt (expanded: https://adminkit.io/llms-full.txt) · Documentation: https://adminkit.io/bootstrap/docs/ · Questions: support@adminkit.io
