The modern, type-safe way to render Quill Deltas anywhere — server, client, or CLI.
Render rich Quill content as HTML, React components, or Markdown without needing the Quill editor or a browser DOM. Fully tree-shakeable. Zero dependencies. Written in TypeScript.
If you're using Quill, you've likely hit one of these walls:
| Problem | How quill-delta-renderer helps |
|---|---|
| "I need to render Deltas on the server." Quill requires a browser DOM, so SSR in Next.js, Nuxt, or plain Node is painful. | Works in any JavaScript runtime — Node, Deno, Bun, edge functions — no DOM required. |
"My React preview re-renders everything on each edit." Using dangerouslySetInnerHTML replaces the entire DOM subtree whenever the Delta changes — no reconciliation, no partial updates. |
The React renderer returns a native ReactNode tree. React only updates the nodes that actually changed, and you can swap in custom components like <CustomImage> or <LinkPreview>. |
| "I need to convert Deltas to Markdown." Mapping rich formatting (colors, underlines, tables) to Markdown is surprisingly hard. | Three Markdown renderers handle this out of the box — strict, HTML-flavored, or bracket-tagged. |
| "Adding a custom embed or block type is way too hard." Extending older converters often means monkey-patching or forking. | An extensible renderer API with full TypeScript autocomplete — register a custom block handler in a few lines. |
import { parseQuillDelta } from "quill-delta-renderer";
import { SemanticHtmlRenderer } from "quill-delta-renderer/html";
const delta = {
ops: [
{ insert: "Hello" },
{ insert: ", world!", attributes: { bold: true } },
{ insert: "\n" },
],
};
const html = new SemanticHtmlRenderer().render(parseQuillDelta(delta));
// → '<p>Hello<strong>, world!</strong></p>'Two lines. Delta in, HTML out. Works the same on the server and in the browser.
npm install quill-delta-rendererThe React renderer is optional — add React only if you use it:
npm install react react-domimport { parseQuillDelta } from "quill-delta-renderer";
import { SemanticHtmlRenderer } from "quill-delta-renderer/html";
const ast = parseQuillDelta(delta);
const html = new SemanticHtmlRenderer().render(ast);Render Deltas directly into a React component tree. By default, it outputs standard HTML tags (<p>, <h1>, <strong>, etc.). You can override any block with your own component:
import { parseQuillDelta } from "quill-delta-renderer";
import { ReactRenderer } from "quill-delta-renderer/react";
const renderer = new ReactRenderer({
components: {
image: ({ node }) => <CustomImage src={node.data} />,
video: ({ node }) => <VideoPlayer url={node.data} />,
paragraph: ({ children, className }) => (
<p className={className}>{children}</p>
),
},
});
const element = renderer.render(parseQuillDelta(delta));
// Use `element` directly in JSX — it's a ReactNodeEvery block type (paragraph, header, blockquote, code-block, list, list-item, image, video, table, table-row, table-cell, formula, mention) can be overridden via components.
import { parseQuillDelta } from "quill-delta-renderer";
import { MarkdownRenderer } from "quill-delta-renderer/markdown";
const md = new MarkdownRenderer().render(parseQuillDelta(delta));Three flavors are available:
| Renderer | Non-standard formats (underline, color, etc.) |
|---|---|
MarkdownRenderer |
Stripped — strict standard Markdown only |
HtmlMarkdownRenderer |
Rendered as inline HTML (<u>, <sub>, <span>) |
BracketMarkdownRenderer |
Rendered as bracket tags ([STYLE color=red]...[/STYLE]) |
See the format docs: HTML Markdown · Bracket Markdown
SemanticHtmlRenderer is designed to produce output compatible with quill-delta-to-html, so migration can be as simple as swapping the import:
Before:
import { QuillDeltaToHtmlConverter } from "quill-delta-to-html";
const converter = new QuillDeltaToHtmlConverter(delta.ops, {});
const html = converter.convert();After:
import { parseQuillDelta } from "quill-delta-renderer";
import { SemanticHtmlRenderer } from "quill-delta-renderer/html";
const html = new SemanticHtmlRenderer().render(parseQuillDelta(delta));What you gain immediately:
- Full TypeScript autocomplete for every config option and custom handler
- Tree-shakeable — import only the renderer you use; unused code is never bundled
- The same extensibility API works across HTML, React, and Markdown renderers
- Significantly faster rendering (see Performance)
Compatibility note: Default configuration targets high compatibility with
quill-delta-to-htmloutput. For edge cases, see the config options (inlineStyles,classPrefix,linkTarget, etc.) and the configuration reference below. We recommend comparing output on a few representative documents during migration.
All renderers accept an optional config object. Every option has a sensible default.
new SemanticHtmlRenderer({
classPrefix: "ql", // CSS class prefix (default: 'ql')
paragraphTag: "p", // Tag for paragraphs (default: 'p')
linkTarget: "_blank", // Link target attribute (default: '_blank')
linkRel: "noopener", // Link rel attribute
inlineStyles: false, // Use inline styles instead of classes
encodeHtml: true, // HTML-encode text content (default: true)
customTag: (fmt, node) => {
/* return a custom tag string or undefined */
},
});new ReactRenderer({
classPrefix: "ql",
linkTarget: "_blank",
linkRel: "noopener",
customTag: (fmt, node) => {
/* return a custom tag string or undefined */
},
components: {
image: ({ node }) => <CustomImage src={node.data} />,
// override any block type
},
});new MarkdownRenderer({
bulletChar: "*", // Unordered list character (default: '*')
fenceChar: "```", // Fenced code block delimiter (default: '```')
embedHandler: (node) => {
/* return string for custom embeds */
},
embedAttributesHandler: (node) => {
/* return { key: value } for attribute-only embeds */
},
});HtmlMarkdownRenderer and BracketMarkdownRenderer accept the same config.
parseQuillDelta(delta, {
extraBlockAttributes: { ... }, // Additional block attribute handlers
blockEmbeds: ['video'], // Block-level embed types (default: ['video'])
extraTransformers: [myGrouper], // Appended after standard transformers
transformers: [...], // Replace standard transformers entirely
});A transformer is a function that receives the AST's children array and returns a new array. Use this to group, wrap, or reorganize nodes before rendering:
import type { TNode, Transformer } from "quill-delta-renderer/core";
const imageGallery: Transformer = (children: TNode[]) => {
// group adjacent images into a gallery container
return groupImages(children);
};
const ast = parseQuillDelta(delta, {
extraTransformers: [imageGallery],
});Quill allows custom embeds (e.g., a custom video player, a mention, or a poll). To render them, first register the embed type in parseQuillDelta, then provide a render handler for your chosen output format:
// 1. Tell the parser about your custom block-level embed
const ast = parseQuillDelta(delta, {
blockEmbeds: ['video', 'poll'], // 'video' is default, add yours
});
// 2. Handle it in your renderer (React example)
const element = new ReactRenderer({
components: {
poll: ({ node }) => <Poll id={node.data.id} />,
},
}).render(ast);For output formats that need HTML-style attribute collection, extend BaseRenderer. For simpler formats (plain text, Markdown-like), extend SimpleRenderer — you only need two methods:
import { SimpleRenderer } from "quill-delta-renderer/core";
class PlainTextRenderer extends SimpleRenderer<string> {
protected joinChildren(children: string[]) {
return children.join("");
}
protected renderText(text: string) {
return text;
}
}Import only what you need — unused renderers are never bundled:
| Import path | Contents |
|---|---|
quill-delta-renderer |
Barrel export including parseQuillDelta |
quill-delta-renderer/core |
parseDelta, BaseRenderer, SimpleRenderer, types |
quill-delta-renderer/common |
Transformers, sanitizers, shared utilities |
quill-delta-renderer/html |
SemanticHtmlRenderer, QuillHtmlRenderer |
quill-delta-renderer/markdown |
MarkdownRenderer, HtmlMarkdownRenderer, BracketMarkdownRenderer |
quill-delta-renderer/react |
ReactRenderer |
For a single blog post, rendering speed is rarely a bottleneck. Where performance matters is SSR throughput — rendering hundreds of pages per second — and bulk processing large document sets.
In those scenarios, quill-delta-renderer delivers measurable gains:
- HTML: 2–5× faster than
quill-delta-to-html(3.65× on a realistic mixed-content document) - React: 1.5–2.9× faster than
quill-delta-to-react(2.22× on a realistic mixed-content document)
Full methodology and results: BENCHMARKS.md
The library is fully typed. We recommend relying on your IDE's IntelliSense to explore the available configuration options, AST node types, and renderer methods.
Contributions are welcome! To get started:
npm install
npm run build
npm testPlease ensure all tests pass and the code is formatted before submitting a pull request.