Quill is a free, open source WYSIWYG rich text editor for the modern web. You give it a container element, and it turns into a powerful editor with formatting, themes, and a clean programmatic API. Quill is built around a structured document model (the Delta format) instead of raw HTML, which makes its content predictable to read, write, and transform across clients and platforms.
This guide is written for the web developer who wants to embed Quill in a page or application. It covers installation, configuration, everyday content and formatting tasks, the built‑in modules and themes, troubleshooting, and a complete API quick reference — all derived from the Quill 2.0.3 source.
Note
Quill is distributed as a browser library (
quillon npm). It has no command‑line interface and no server component — everything runs in the browser. If you are looking for deployment, build, and architecture details, see the companion Admin Guide.
1. Quick Start
The fastest way to get an editor on the page is the CDN full build with the snow theme.
<!-- Include the snow theme stylesheet -->
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" />
<!-- Create the editor container with some initial content -->
<div id="editor">
<p>Hello World!</p>
<p>Some initial <strong>bold</strong> text</p>
<p><br /></p>
</div>
<!-- Include the Quill library -->
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<!-- Initialize the editor -->
<script>
const quill = new Quill('#editor', {
theme: 'snow'
});
</script>
When the page loads, the contents of #editor become the editor's initial document, and a formatting toolbar appears above it.
A bordered editor with a toolbar (headers, bold, italic, underline, link,
lists, clean) above an editable area pre-filled with "Hello World!".
Tip
The element you point Quill at is replaced by the editor — its existing inner HTML becomes the starting document. Keep a separate element for anything you do not want consumed.
2. Installation
Quill ships in several forms. Pick the one that matches how your project loads JavaScript.
2.1 Choosing a build
Quill offers two builds:
| Build | Entry | What it includes |
|---|---|---|
| Full | quill (npm) / quill.js (CDN) |
Core library plus common themes (snow, bubble), all standard formats, and default modules. The easiest way to start. |
| Core | quill/core (npm) / quill.core.js (CDN) |
Only the core library. You register the formats, modules, and themes you need yourself. Smallest footprint. |
Important
Importing
"quill"anywhere in your project registers the full build's formats and modules. If you want the slim core build, import"quill/core"consistently and never import"quill"directly.
2.2 CDN
A globally distributed CDN is available, backed by jsDelivr. This requires no build step or package manager.
Full build (snow theme)
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
<div id="editor">
<h2>Demo Content</h2>
<p>Preset build with the <code>snow</code> theme.</p>
</div>
<script>
const quill = new Quill('#editor', { theme: 'snow' });
</script>
Core build (no theme)
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.core.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.core.js"></script>
<div id="editor">
<p>Core build with no theme or non-essential modules.</p>
</div>
<script>
const quill = new Quill('#editor');
</script>
CDN builds expose Quill on the global window object. From there, Quill.import() gives you access to internal components (formats, modules, themes, the Delta class).
2.3 npm
If your project uses a bundler such as Webpack or Vite, install from npm:
npm install quill@2.0.3
added 1 package, and audited N packages in Xs
Import the full build, or the core build:
import Quill from 'quill';
// Or, for the slim core build:
// import Quill from 'quill/core';
const quill = new Quill('#editor');
In a bundler environment you can import individual formats and modules directly, which is more natural than Quill.import():
import Quill from 'quill';
import { Delta } from 'quill'; // the Delta document type
import Link from 'quill/formats/link'; // a single format
// Equivalent: const Delta = Quill.import('delta');
// const Link = Quill.import('formats/link');
2.4 Styles
The npm package bundles the same stylesheets as the CDN, in the dist directory. Import the one matching your theme:
import 'quill/dist/quill.snow.css'; // snow theme
// import 'quill/dist/quill.bubble.css'; // bubble theme
// import 'quill/dist/quill.core.css'; // core build, no theme styling
| Stylesheet | Use with |
|---|---|
quill.core.css |
Core build / no theme. Provides only essential editor styling. |
quill.snow.css |
snow theme (toolbar above the editor). Includes core styling. |
quill.bubble.css |
bubble theme (inline tooltip toolbar). Includes core styling. |
Warning
The theme stylesheet is not loaded automatically when you set
theme: 'snow'. You must include the matching CSS yourself, or the editor will render unstyled.
2.5 Requirements
Quill runs in any modern browser environment that provides the standard DOM APIs it relies on (DOMParser, MutationObserver, Selection/Range, beforeinput events). The npm package targets the defaults browserslist. There are no other runtime dependencies you need to install — parchment, quill-delta, eventemitter3, and lodash-es are bundled.
3. Your First Editor in Depth
const quill = new Quill('#editor', {
theme: 'snow',
placeholder: 'Compose an epic...',
readOnly: false,
});
The Quill constructor takes two arguments:
- A container — a CSS selector string (the first match is used) or a DOM element. If the container is not empty, its current contents become the initial document.
- An options object (optional) — see Configuration.
// Selector string
const quill = new Quill('#editor');
// DOM element
const container = document.getElementById('editor');
const quill = new Quill(container);
Caution
If the selector matches nothing, or the element is otherwise invalid, Quill throws
Invalid Quill container. Make sure the element exists in the DOM before you construct the editor.
4. Configuration
Pass an options object as the second argument to customize the editor. Every key is optional.
const quill = new Quill('#editor', {
debug: 'info',
modules: {
toolbar: true,
},
placeholder: 'Compose an epic...',
theme: 'snow',
});
| Option | Type | Default | Description |
|---|---|---|---|
theme |
string |
minimal default | Name of the theme: "snow" or "bubble". An invalid or falsy value loads a minimal default theme (no toolbar styling). The theme's stylesheet must still be included manually. |
modules |
object |
see Modules | Configuration for built‑in and custom modules (toolbar, keyboard, history, etc.). |
placeholder |
string |
"" |
Text shown when the editor is empty. |
readOnly |
boolean |
false |
If true, the editor starts in read‑only mode (no user edits). |
bounds |
HTMLElement | string | null |
document.body |
Element (or selector) within which UI elements like tooltips are confined. Only left/right boundaries are considered. |
formats |
string[] | null |
null |
Allowed format names. null means all registered formats are allowed. Pass an array to restrict formatting. Ignored if registry is set. |
registry |
Parchment.Registry |
shared global | A custom Parchment registry for advanced format/blot customization. Specifying this ignores the formats option. |
debug |
DebugLevel | boolean |
'warn' |
Shortcut for Quill.debug(). Sets logging verbosity. Static — affects all editors on the page. |
4.1 Restricting formats
To allow only a subset of formats, pass their names:
const quill = new Quill('#editor', {
formats: ['italic'], // only italic is permitted in the contents
});
const Delta = Quill.import('delta');
quill.setContents(
new Delta()
.insert('Only ')
.insert('italic', { italic: true })
.insert(' is allowed. ')
.insert('Bold', { bold: true }) // bold is stripped
.insert(' is not.')
);
Note
Restricting formats affects what can exist in the document, including content pasted from outside. Disallowed formats are removed rather than rejected.
4.2 Read‑only editors
const quill = new Quill('#editor', {
readOnly: true,
modules: { toolbar: null }, // hide the toolbar entirely
theme: 'snow',
});
You can toggle the mode at runtime with quill.enable() / quill.disable().
5. Understanding Content: Delta vs. HTML
Quill represents documents internally as a Delta — a JSON structure describing content as a sequence of insert operations with optional formatting attributes. Deltas are the canonical, lossless representation of editor content; HTML is treated as an import/export format.
const Delta = Quill.import('delta');
const contents = new Delta()
.insert('Gandalf', { bold: true })
.insert(' the ')
.insert('Grey', { color: '#cccccc' })
.insert('\n');
This Delta describes the text "Gandalf the Grey" with "Gandalf" bold and "Grey" colored grey, followed by a newline (which ends the line/paragraph).
Tip
Prefer Deltas over HTML whenever you can. They are unambiguous, diff‑able, and safe to store. Reach for HTML only at the boundaries of your app (e.g. importing pasted content or rendering a static export).
Key things to know:
- Newlines (
\n) delimit lines, and line‑level formats (header, list, alignment) attach to the newline character of the line they format. getContents()returns a Delta;setContents()accepts one.updateContents()applies a change Delta (withretain/delete/insertops) on top of the current document.
6. Everyday Usage
All of the methods below are instance methods on your quill object. Most content‑modifying methods accept either an (index, length) pair or a selection Range, and an optional source ('api', 'user', or 'silent') that controls event emission.
6.1 Inserting and deleting text
// Insert plain text at index 0
quill.insertText(0, 'Hello');
// Insert formatted text
quill.insertText(5, ' World', 'bold', true);
quill.insertText(11, '!', { italic: true, color: 'red' });
// Delete 6 characters starting at index 5
quill.deleteText(5, 6);
6.2 Replacing all content
const Delta = Quill.import('delta');
// Replace the whole document with a Delta
quill.setContents(new Delta().insert('A fresh start\n'));
// Replace the whole document with plain text
quill.setText('Just plain text\n');
6.3 Applying a change on top of existing content
const Delta = Quill.import('delta');
// Keep the first 6 characters, then insert bold "World"
quill.updateContents(
new Delta()
.retain(6)
.insert('World', { bold: true })
);
6.4 Reading content out
quill.getText(); // plain text of the whole document
quill.getText(0, 5); // plain text of a range
quill.getContents(); // Delta of the whole document
quill.getContents(0, 5); // Delta of a range
quill.getLength(); // length of the document (includes the trailing \n)
quill.getSemanticHTML(); // clean, semantic HTML export of the contents
Note
getSemanticHTML()produces clean HTML derived from the Delta, suitable for display or storage — it is not the editor's raw internal DOM. See the Troubleshooting section for known edge cases with lists.
6.5 Formatting
// Format the current selection (inline or line format, auto-detected)
quill.format('bold', true);
quill.format('align', 'center'); // line-level format
// Format a specific range of text (inline)
quill.formatText(0, 5, 'bold', true);
quill.formatText(0, 5, { italic: true, underline: true });
// Format whole lines touched by a range (block/line formats)
quill.formatLine(0, 10, 'header', 2);
quill.formatLine(0, 10, 'list', 'bullet');
// Remove all formatting from a range
quill.removeFormat(0, 12);
// Inspect formatting
quill.getFormat(); // formats of current selection
quill.getFormat(0, 5); // formats common to a range
Tip
Use
formatTextfor inline formats (bold, italic, color, link) andformatLinefor line/block formats (header, list, blockquote, align, indent, code‑block).format()figures out which kind you mean based on the registered format's scope.
6.6 Embeds (images, videos, formulas)
// Insert an image embed at index 10
quill.insertEmbed(10, 'image', 'https://example.com/cat.png');
// Insert a video embed
quill.insertEmbed(0, 'video', 'https://www.youtube.com/embed/QHH3iSeDBLo');
6.7 Selection and focus
quill.setSelection(0, 5); // select characters 0–5
quill.setSelection(10); // collapsed cursor at index 10
quill.getSelection(); // current Range, or null if not focused
quill.getSelection(true); // focus first, then return the Range
quill.focus(); // focus the editor
quill.blur(); // remove focus
quill.hasFocus(); // boolean
quill.getBounds(0, 5); // pixel bounds of a range (for positioning UI)
A Range is simply { index, length }. A collapsed cursor has length === 0.
6.8 Enabling and disabling
quill.disable(); // read-only
quill.enable(); // editable again
quill.enable(false); // same as disable()
quill.isEnabled(); // boolean
7. Listening for Changes (Events)
Register handlers with quill.on(eventName, handler). Quill emits three events, all exposed as constants on Quill.events.
| Event constant | String | Fired when | Handler signature |
|---|---|---|---|
Quill.events.TEXT_CHANGE |
'text-change' |
The document contents change | (delta, oldContents, source) |
Quill.events.SELECTION_CHANGE |
'selection-change' |
The cursor or selection moves | (range, oldRange, source) |
Quill.events.EDITOR_CHANGE |
'editor-change' |
Either of the above (fires first, including for 'silent' text changes) |
(eventName, ...args) |
quill.on('text-change', (delta, oldDelta, source) => {
if (source === 'user') {
console.log('A user edited the document:', delta);
}
});
quill.on('selection-change', (range, oldRange, source) => {
if (range === null) {
console.log('Editor lost focus');
} else {
console.log('Selection is now', range.index, range.length);
}
});
Manage handlers with:
quill.once('text-change', handler); // fire at most once
quill.off('text-change', handler); // remove a handler
7.1 Event sources
Every change carries a source:
| Source | Constant | Meaning |
|---|---|---|
'user' |
Quill.sources.USER |
The change originated from direct user interaction (typing, toolbar click, paste). |
'api' |
Quill.sources.API |
The change came from your code calling an API method. This is the default for most methods. |
'silent' |
Quill.sources.SILENT |
The change should not emit text-change / selection-change (it still emits editor-change). |
Tip
Filter on
source === 'user'when you want to react only to human edits — for example, to mark a document "dirty" or to sync changes to a server. API‑driven changes you made yourself usually shouldn't trigger that logic.
8. Modules
Modules add functionality. Configure them under the modules option. Several are enabled by default; others you opt into.
const quill = new Quill('#editor', {
modules: {
toolbar: '#toolbar',
history: { delay: 2000, maxStack: 500, userOnly: true },
syntax: true,
},
theme: 'snow',
});
Set a module to true to enable it with defaults, to an options object to customize, or to false/null to disable it.
| Module | Default state | Purpose |
|---|---|---|
clipboard |
on | Handles copy / cut / paste, including HTML normalization from Word and Google Docs. |
keyboard |
on | Keyboard shortcuts and custom key bindings. |
history |
on | Undo / redo stack. |
uploader |
on | Drag‑and‑drop / paste image upload. |
toolbar |
added by theme | The formatting toolbar. |
syntax |
off | Syntax‑highlighted code blocks (requires highlight.js). |
table |
off | Editable tables. |
8.1 Toolbar
The toolbar is the most commonly configured module. You can provide a container selector, an array configuration, or an existing DOM element.
// 1. Simple array config — Quill builds the toolbar HTML for you
const quill = new Quill('#editor', {
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
[{ align: [] }],
['link', 'image', 'video'],
['clean'],
],
},
theme: 'snow',
});
// 2. Point at your own toolbar HTML
const quill = new Quill('#editor', {
modules: { toolbar: '#my-toolbar' },
theme: 'snow',
});
<div id="my-toolbar">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<select class="ql-header">
<option value="1"></option>
<option value="2"></option>
<option selected></option>
</select>
<button class="ql-clean"></button>
</div>
Built‑in toolbar controls map to formats by the ql-{format} class. The toolbar ships with handlers for the special controls below; the rest simply toggle their named format.
| Control | Behavior |
|---|---|
clean |
Removes inline formatting from the selection (or all inline formats at the cursor). |
link |
Prompts for a URL (in the bare toolbar) and applies the link format. |
list |
Applies ordered, bullet, or toggles a checked/unchecked checklist (value: 'check'). |
indent |
'+1' / '-1' increases or decreases indentation (direction‑aware). |
direction |
Sets text direction ('rtl') and coordinates alignment. |
Note
The
snowandbubblethemes provide a default toolbar if you enable the toolbar without specifying a container.snow's default is headers + bold/italic/underline/link + ordered/bullet lists + clean.bubble's default is bold/italic/link + header 1/header 2/blockquote.
Custom toolbar handlers
Override what a toolbar button does:
const quill = new Quill('#editor', {
modules: {
toolbar: {
container: '#toolbar',
handlers: {
bold(value) {
// `this` is the toolbar module; `value` is the new active state
this.quill.format('bold', value);
},
},
},
},
});
8.2 Keyboard
The keyboard module powers shortcuts and lets you add custom key bindings. Add a binding with quill.keyboard.addBinding:
quill.keyboard.addBinding(
{ key: 'b', shortKey: true }, // shortKey = Cmd on macOS, Ctrl elsewhere
(range, context) => {
quill.format('bold', !context.format.bold);
}
);
// With a context condition (only when the selection is collapsed at offset 0)
quill.keyboard.addBinding(
{ key: 'Backspace' },
{ collapsed: true, offset: 0 },
(range, context) => {
// custom backspace behavior
}
);
Binding keys include key (a key name, character, or key code), plus optional modifiers: shortKey, shiftKey, altKey, metaKey, ctrlKey. A handler returning true lets the default behavior proceed; returning false (or nothing meaningful) prevents it.
Note
Quill already binds
Enter,Backspace/Delete(with special list/indent handling), and — via the history module — undo (Cmd/Ctrl+Z), redo (Cmd/Ctrl+Shift+Z, plusCtrl+Yon Windows).
8.3 History (undo / redo)
const quill = new Quill('#editor', {
modules: {
history: {
delay: 1000, // ms window in which consecutive changes are merged
maxStack: 100, // maximum number of undo entries kept
userOnly: false // if true, only 'user'-source changes are undoable
},
},
});
quill.history.undo();
quill.history.redo();
quill.history.clear();
| Option | Default | Description |
|---|---|---|
delay |
1000 |
Changes within this many milliseconds are merged into a single undo step. |
maxStack |
100 |
Maximum undo/redo entries retained. |
userOnly |
false |
When true, only user edits are recorded; API changes are transformed but not undoable (useful for collaborative editing). |
Tip
Set
userOnly: truein collaborative apps so a user's undo never reverts a change that came from a remote collaborator.
8.4 Clipboard
The clipboard module governs paste behavior, normalizing messy HTML (including content pasted from Microsoft Word and Google Docs) into clean Deltas. You can extend it with custom matchers:
// Strip formatting from anything pasted by matching all element nodes
const quill = new Quill('#editor', {
modules: {
clipboard: {
matchers: [
[Node.ELEMENT_NODE, (node, delta) => {
return delta.compose(new Delta().retain(delta.length(), {
color: false, background: false,
}));
}],
],
},
},
});
// Programmatic paste of HTML
quill.clipboard.dangerouslyPasteHTML(0, '<b>Bold</b> text');
Caution
dangerouslyPasteHTMLinserts HTML through the clipboard's normalization but is named "dangerously" for a reason: never pass unsanitized, untrusted HTML to it. See the Admin Guide → Security section.
8.5 Syntax highlighting
The syntax module highlights code blocks. It requires the third‑party highlight.js library to be available on the page.
<link href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/atom-one-dark.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script>
const quill = new Quill('#editor', {
modules: {
syntax: true, // uses window.hljs by default
toolbar: [['code-block']],
},
theme: 'snow',
});
| Option | Default | Description |
|---|---|---|
hljs |
window.hljs |
The highlight.js instance to use. |
interval |
1000 |
How often (ms) highlighting re‑runs after edits. |
languages |
Plain, Bash, C++, C#, CSS, Diff, HTML/XML, Java, JavaScript, Markdown, PHP, Python, Ruby, SQL | The language options shown in the code‑block language picker. |
Warning
If you enable
syntaxwithout highlight.js loaded, Quill throws:Syntax module requires highlight.js. Please include the library on the page before Quill.
8.6 Tables
The table module adds editable tables and an API for manipulating them.
const quill = new Quill('#editor', {
modules: { table: true },
theme: 'snow',
});
const table = quill.getModule('table');
table.insertTable(3, 3); // 3 rows × 3 columns
table.insertRowAbove();
table.insertRowBelow();
table.insertColumnLeft();
table.insertColumnRight();
table.deleteRow();
table.deleteColumn();
table.deleteTable();
8.7 Image upload (uploader)
The uploader module handles images dropped onto, or pasted into, the editor. By default it accepts image/png and image/jpeg, reads them as data URLs, and inserts them as image embeds. Provide a custom handler to upload to your server instead:
const quill = new Quill('#editor', {
modules: {
uploader: {
mimetypes: ['image/png', 'image/jpeg', 'image/gif'],
handler(range, files) {
// upload `files` to your server, then insert the returned URLs
},
},
},
});
8.8 Accessing a module instance
const toolbar = quill.getModule('toolbar');
const history = quill.getModule('history');
9. Themes
A theme controls the editor's look and its toolbar/tooltip UI. Quill bundles two themes plus a minimal default.
| Theme | theme value |
Stylesheet | Character |
|---|---|---|---|
| Snow | 'snow' |
quill.snow.css |
A clean, flat toolbar fixed above the editor. The most common choice. |
| Bubble | 'bubble' |
quill.bubble.css |
A tooltip toolbar that appears over the selection. Minimalist, Medium‑style. |
| Default | omitted / invalid | quill.core.css |
No toolbar styling. You build your own UI. |
const quill = new Quill('#editor', { theme: 'bubble' });
Important
Setting
themedoes not load CSS. Always include the matching stylesheet for the theme you choose.
10. Common Recipes
10.1 Save content to your server
quill.on('text-change', (delta, oldDelta, source) => {
if (source !== 'user') return;
const contents = quill.getContents(); // a Delta — store this
fetch('/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(contents),
});
});
Tip
Store the Delta, not HTML. To re‑hydrate, call
quill.setContents(savedDelta). To render the document outside Quill (e.g. in an email), generate HTML withquill.getSemanticHTML()at export time.
10.2 Build a live HTML preview
const preview = document.getElementById('preview');
quill.on('text-change', () => {
preview.innerHTML = quill.getSemanticHTML();
});
10.3 Enforce a maximum length
quill.on('text-change', () => {
const limit = 280;
if (quill.getLength() > limit) {
quill.deleteText(limit - 1, quill.getLength());
}
});
10.4 Programmatically focus and place the cursor at the end
quill.focus();
quill.setSelection(quill.getLength(), 0);
10.5 Sync a hidden form field on submit
const editor = document.querySelector('#editor');
const hidden = document.querySelector('input[name="body"]');
document.querySelector('form').addEventListener('submit', () => {
hidden.value = JSON.stringify(quill.getContents());
});
11. Troubleshooting
Error: Invalid Quill container
Cause. The container selector matched no element, or null/undefined was passed as the container.
Solution. Confirm the element exists in the DOM before constructing Quill — initialize after DOMContentLoaded, or place the script after the container element.
Prevention. Use a specific, unique selector and verify it with document.querySelector('#editor') first.
Error: Invalid theme {name}. Did you register it?
Cause. You set theme to a name that is not registered. Only 'snow' and 'bubble' are built in.
Solution. Use 'snow', 'bubble', or omit theme. If you wrote a custom theme, register it with Quill.register('themes/myTheme', MyTheme) before constructing the editor.
Error: Syntax module requires highlight.js. Please include the library on the page before Quill.
Cause. The syntax module was enabled but window.hljs (highlight.js) is not present.
Solution. Load highlight.js before initializing Quill, or pass an explicit instance via modules.syntax.hljs.
The editor renders but has no toolbar styling
Cause. The theme stylesheet is missing.
Solution. Include quill.snow.css (or quill.bubble.css) matching your theme.
Prevention. Treat the theme JS option and its CSS file as a pair — always ship both.
Pasting from Word or Google Docs loses or mangles list formatting
Cause. External HTML (especially from Microsoft Word and Google Docs) is irregular. Quill normalizes it, but some edge cases — particularly bullet vs. numbered lists and empty lines — are imperfect.
Solution. Add custom clipboard matchers for the specific patterns you need to preserve. For checklists/ordered lists, verify the result and adjust with formatLine(..., 'list', ...) after paste if needed.
Prevention. Where exact fidelity matters, paste as plain text and re‑apply formats, or constrain allowed formats with the formats option.
getSemanticHTML() output for nested lists isn't what I expect
Cause. Semantic HTML generation for deeply nested or mixed list types is an area with known rough edges.
Solution. If you rely on exact list HTML, post‑process the output, or render from the Delta directly with your own logic.
Typing is slow in very large documents
Cause. Extremely large documents and heavy modules (e.g. syntax highlighting on huge code blocks) increase per‑keystroke work.
Solution. Increase modules.syntax.interval, split very large documents, or disable heavy modules where they are not needed.
Toolbar button does nothing / ignoring attaching to nonexistent format
Cause. A toolbar control references a format that is not registered (common with the core build).
Solution. Register the format (e.g. Quill.import('formats/bold') then Quill.register(...)) or remove the control. The full build registers all standard formats.
Read‑only editor still accepts programmatic edits
Cause. readOnly/disable() blocks user edits (source: 'user'), not API calls. This is by design.
Solution. This is expected — API methods still work so your code can update a read‑only view. To run an edit that should apply even to user‑blocked state, that is what the internal read‑only edit path is for; otherwise simply gate your own API calls.
Editor loses selection after an API change
Cause. Some API changes move the selection. Quill attempts to preserve it, but complex updates may shift it.
Solution. Capture quill.getSelection() before the change and restore it with quill.setSelection(range, 'silent') afterward.
12. FAQ
Q: Does Quill store content as HTML?
No. The canonical format is Delta (a JSON op list). HTML is only for import (paste) and export (getSemanticHTML()). Store Deltas.
Q: How do I get the HTML out of the editor?
Use quill.getSemanticHTML() for clean, semantic HTML. (The editor's live DOM is an implementation detail and should not be scraped directly.)
Q: How do I set content from saved data?
quill.setContents(savedDelta). For plain text, quill.setText('...').
Q: How do I make the editor read‑only?
Set readOnly: true at construction, or call quill.disable() / quill.enable() at runtime.
Q: How do I add a custom keyboard shortcut?
quill.keyboard.addBinding({ key: 'k', shortKey: true }, handler).
Q: Can I limit which formats are allowed?
Yes — pass formats: ['bold', 'italic', ...]. Anything not listed is stripped, including on paste.
Q: Where do I put my own toolbar?
Render the toolbar HTML yourself and point modules.toolbar at its selector, or pass an array config and let Quill build it.
Q: Why isn't my theme styled?
You set theme but didn't include the theme's CSS file. Add quill.snow.css or quill.bubble.css.
Q: What's the difference between format, formatText, and formatLine?
format applies to the current selection and auto‑detects inline vs. line scope; formatText targets a range with inline formats; formatLine targets whole lines with block formats.
Q: How do I detect only human edits?
Check source === 'user' in your text-change handler.
Q: How do I insert an image?
quill.insertEmbed(index, 'image', url), or let the uploader module handle drag‑and‑drop / paste.
Q: Does Quill support tables?
Yes, via the table module (off by default). Enable it and use quill.getModule('table') for row/column operations.
Q: How do I undo/redo programmatically?
quill.history.undo() and quill.history.redo().
Q: Which browsers are supported?
Modern browsers matching the defaults browserslist. Quill relies on standard DOM APIs (MutationObserver, Selection/Range, beforeinput).
Q: How do I confine tooltips to a container?
Set the bounds option to the element (or selector) you want them constrained within.
Q: Is there a CDN?
Yes — a jsDelivr‑backed CDN serves quill.js, quill.core.js, and the theme stylesheets.
13. API Quick Reference
Constructor
new Quill(container: string | HTMLElement, options?: QuillOptions)
Content
| Method | Description |
|---|---|
getText(index?, length?) |
Plain text of the document or a range. |
getContents(index?, length?) |
Delta of the document or a range. |
getLength() |
Document length (includes trailing \n). |
getSemanticHTML(index?, length?) |
Clean semantic HTML export. |
setText(text, source?) |
Replace contents with plain text. |
setContents(delta, source?) |
Replace contents with a Delta. |
updateContents(delta, source?) |
Apply a change Delta. |
insertText(index, text, [formats], source?) |
Insert text, optionally formatted. |
deleteText(index, length, source?) |
Delete a range. |
insertEmbed(index, type, value, source?) |
Insert an embed (image, video, formula). |
Formatting
| Method | Description |
|---|---|
format(name, value, source?) |
Format the current selection. |
formatText(index, length, name/formats, [value], source?) |
Apply inline format(s) to a range. |
formatLine(index, length, name/formats, [value], source?) |
Apply line format(s). |
removeFormat(index, length, source?) |
Strip formatting from a range. |
getFormat(index?/range?, length?) |
Formats of the selection or range. |
Selection
| Method | Description |
|---|---|
getSelection(focus?) |
Current Range (or null). |
setSelection(index, [length], source?) |
Set the selection/cursor. |
getBounds(index/range, length?) |
Pixel bounds of a position. |
focus() / blur() |
Focus / unfocus the editor. |
hasFocus() |
Whether the editor has focus. |
getLine(index) / getLeaf(index) / getLines(index?, length?) |
Inspect the document structure. |
getIndex(blot) |
Document index of a blot. |
Editor state & events
| Method | Description |
|---|---|
enable(enabled?) / disable() |
Toggle editability. |
isEnabled() |
Whether editing is allowed. |
update(source?) |
Force a synchronous update from the DOM. |
on(event, handler) / once(...) / off(...) |
Event subscription. |
getModule(name) |
Get a module instance. |
scrollSelectionIntoView() |
Scroll the selection into view. |
Static members
| Member | Description |
|---|---|
Quill.register(path, target, overwrite?) |
Register a format, module, or theme. |
Quill.import(name) |
Retrieve a registered component ('delta', 'parchment', 'formats/...', etc.). |
Quill.find(node, bubble?) |
Find the Quill instance or blot for a DOM node. |
Quill.debug(level) |
Set global debug level ('error', 'warn', 'log', 'info', or boolean). |
Quill.version |
The library version string. |
Quill.events / Quill.sources |
Event and source name constants. |
Built‑in formats
Inline: bold, italic, underline, strike, link, color, background, font, size, script (sub/super), code.
Block/line: header (H1–H6), blockquote, code-block, list (ordered/bullet/checked/unchecked), indent, align, direction.
Embeds: image, video, formula, table.