Quill Rich Text Editor, Custom Toolbar — Free JS Snippet

Quill Rich Text Editor with Custom Toolbar and Character Limit · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

What's included

Features

Fully custom HTML toolbar wired up through the container option
Custom button that sits beside built-in formatting controls
formats whitelist that strips unsupported formatting on paste
Hard character limit enforced with a silent deleteText to avoid loops
Live counter with amber and red warning states
Live HTML and Delta (JSON) output tabs
Initial content loaded from a Delta with setContents
Accessible labels on every toolbar button

About this UI Snippet

Quill Rich Text Editor with Custom Toolbar — HTML, CSS & JavaScript

Screenshot of the Quill Rich Text Editor with Custom Toolbar and Character Limit snippet rendered live

Rich text editing in the browser is notoriously hard. The old approach — contenteditable plus document.execCommand — behaves differently in every browser and produces messy, inconsistent HTML. Quill takes a different route. It treats the document as data, a structured model called a Delta, and renders the editor from it, which is what makes its output predictable and its features (undo, formatting, collaboration) reliable. This snippet is Quill 1.3.7, the long-standing stable release that loads from a single script tag and is still widely deployed.

The default toolbar is generic, and production forms almost always want their own. Quill lets you pass modules: { toolbar: { container: '#selector' } } and then build the toolbar yourself in plain HTML using its class conventions — ql-bold, ql-italic, ql-list with a value attribute, ql-header on a select. Quill wires those elements to formatting automatically, and because it is your markup, you can restyle it, reorder it, and add controls of your own. The Sign button here has no ql- class at all; it is an ordinary button whose click handler calls quill.insertText, showing how custom actions coexist with built-in ones.

The formats option is the security-minded setting. Pasted content can carry fonts, colours, sizes and other formatting you never intended to support; a whitelist of header, bold, italic, underline, list, blockquote and link makes Quill discard everything else on paste, keeping content consistent with the design. Two implementation details matter for the character limit. Quill's document always ends with a newline, so the real length is getLength() minus one. And the limit is enforced in the text-change handler by trimming with deleteText using the silent source, which avoids the handler re-triggering itself in a loop. The counter changes colour as the limit approaches — amber at 90 percent, red once exceeded.

The two output tabs show what Quill actually gives you. quill.root.innerHTML is the rendered HTML you would store or display, and quill.getContents() is the Delta — a JSON list of inserts with attributes — which is the better thing to persist if you plan to edit the content again, since it round-trips exactly and is easy to diff. The initial document is loaded through setContents with a Delta, which is also how you would restore saved content.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add an image upload handler, @mention autocomplete, or sanitise the HTML output with DOMPurify before saving.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a rich text editor with Quill 1.3.7 loaded from a CDN (script and snow CSS).

Requirements:
- Write the toolbar in HTML (header select, bold, italic, underline, ordered and bullet list, blockquote, link, clean) and pass it via modules: { toolbar: { container: '#toolbar' } }.
- Add a custom button (not a ql- class) whose handler inserts an italic signature at the cursor with insertText and moves the selection after it.
- Restrict formats to header, bold, italic, underline, list, blockquote and link.
- Enforce a 500 character limit in the text-change handler using deleteText with the 'silent' source, remembering that getLength() includes a trailing newline; show a counter that turns amber at 90% and red over the limit.
- Show tabs for quill.root.innerHTML and JSON.stringify(quill.getContents()), and load the starting text with setContents.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="qe-card">
  <div class="qe-head">
    <h3>Write a post</h3>
    <span class="qe-count" id="qeCount" aria-live="polite">0 / 500</span>
  </div>
  <div id="qeToolbar" class="qe-toolbar">
    <span class="ql-formats">
      <select class="ql-header"><option value="1">Heading</option><option value="2">Subheading</option><option selected>Normal</option></select>
    </span>
    <span class="ql-formats">
      <button class="ql-bold" aria-label="Bold"></button>
      <button class="ql-italic" aria-label="Italic"></button>
      <button class="ql-underline" aria-label="Underline"></button>
    </span>
    <span class="ql-formats">
      <button class="ql-list" value="ordered" aria-label="Numbered list"></button>
      <button class="ql-list" value="bullet" aria-label="Bulleted list"></button>
      <button class="ql-blockquote" aria-label="Quote"></button>
    </span>
    <span class="ql-formats">
      <button class="ql-link" aria-label="Link"></button>
      <button class="ql-clean" aria-label="Clear formatting"></button>
    </span>
    <span class="ql-formats">
      <button type="button" class="qe-custom" id="qeSig" title="Insert signature">&#9998; Sign</button>
    </span>
  </div>
  <div id="qeEditor"></div>
  <div class="qe-tabs" role="tablist">
    <button type="button" role="tab" aria-selected="true" data-v="html">HTML output</button>
    <button type="button" role="tab" aria-selected="false" data-v="delta">Delta (JSON)</button>
  </div>
  <pre class="qe-out" id="qeOut"></pre>
</div>

Step by step

How to Use

  1. 1
    Format some textSelect a word and use the toolbar to bold, italicise or link it. Change the heading level with the dropdown.
  2. 2
    Use the custom buttonClick Sign to insert an italic signature at the cursor. It is a normal button, not a built-in Quill control.
  3. 3
    Watch the counterType or paste past 450 characters and the counter turns amber; past 500 the extra text is trimmed.
  4. 4
    Compare the outputsSwitch between HTML output and Delta (JSON) to see the two representations of the same document.
  5. 5
    Paste formatted contentPaste text with fonts and colours from another page; unsupported formats are stripped.

Real-world uses

Common Use Cases

CMS and blog editors
Give authors a friendly editor that outputs consistent HTML. For markdown-based writing see the Markdown live preview.
Comment and review forms
Offer basic formatting with a strict limit and predictable output.
ADMIN
Email and notification templates
Let staff compose formatted messages within a safe set of styles.
Learning document models
Shows why a Delta-based editor is more reliable than raw contenteditable.

Got questions?

Frequently Asked Questions

A Delta is Quill's JSON document format: an ordered list of insert operations with optional attributes. It round-trips exactly and is easy to store and diff.

Pass modules: { toolbar: { container: "#toolbar" } } and write the toolbar markup using ql- classes such as ql-bold and ql-list with value attributes.

Quill always keeps a final newline in the document, so the visible character count is getLength() minus one.

Set the formats option to a whitelist. Anything not on the list is discarded.

Store the Delta if you will edit the content again, since it round-trips exactly. Store HTML for display or when another system needs it.

Version 1.3.7 is stable and widely used. Quill 2 is the newer release with a similar API and modern build output.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page to convert the markup and styles. The behaviour comes from Quill, so in a framework project install it with npm install quill (or react-quill / ngx-quill) instead of the CDN tag, create it in useEffect / onMounted / ngAfterViewInit on the editor element, and release it with removing the element and its listeners when the component unmounts.