Rich Text Editor: WYSIWYG Implementation
Learn: Rich Text Editor: WYSIWYG Implementation
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Rich Text Editor: WYSIWYG Implementation with Quill & Slate
Problem
Building modern web applications often requires rich text editing capabilities. Standard <textarea> elements are insufficient for:
- Formatting needs: Bold, italic, lists, links, images
- User experience: WYSIWYG (What You See Is What You Get) editing
- Data management: Structured content storage and retrieval
- Extensibility: Custom plugins and toolbar customization
- Performance: Handling large documents efficiently
Developers face choices between lightweight solutions (Quill) and highly customizable frameworks (Slate).
Solution Overview
Quill: Lightweight & Production-Ready
Best for: Quick implementation, standard formatting needs, smaller teams
Advantages:
- Minimal setup required
- Rich API out-of-the-box
- Delta format (JSON-based, version-control friendly)
- Excellent documentation
- Smaller bundle size (~43KB gzipped)
Slate: Powerful & Flexible
Best for: Complex requirements, custom workflows, enterprise applications
Advantages:
- Completely customizable rendering
- Plugin architecture
- Full control over data model
- React-native support
- Headless design (no opinionated UI)
Code Implementation
1. Quill Implementation
Installation
npm install quill
Basic Setup
import React, { useRef, useEffect, useState } from 'react';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
export function QuillEditor() {
const editorRef = useRef(null);
const quillRef = useRef(null);
const [content, setContent] = useState('');
useEffect(() => {
quillRef.current = new Quill(editorRef.current, {
theme: 'snow',
placeholder: 'Start typing...',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image'],
['clean']
]
}
});
quillRef.current.on('text-change', () => {
const delta = quillRef.current.getContents();
setContent(JSON.stringify(delta));
});
}, []);
const handleSave = () => {
console.log('Saved content:', content);
};
return (
<div>
<div ref={editorRef} style={{ height: '400px' }} />
<button onClick={handleSave}>Save</button>
</div>
);
}
Advanced Quill with Custom Formats
import Quill from 'quill';
// Register custom format
const Inline = Quill.import('blots/inline');
class HighlightBlot extends Inline {
static blotName = 'highlight';
static tagName = 'mark';
static className = 'highlight';
}
Quill.register(HighlightBlot);
export function AdvancedQuillEditor() {
const editorRef = useRef(null);
const quillRef = useRef(null);
useEffect(() => {
quillRef.current = new Quill(editorRef.current, {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ 'color': [] }, { 'background': [] }],
['highlight'],
['link', 'image', 'video']
],
handlers: {
'highlight': function() {
const format = this.quill.getFormat();
this.quill.formatText(
this.quill.getSelection(),
'highlight',
!format.highlight
);
}
}
}
}
});
}, []);
return <div ref={editorRef} style={{ height: '500px' }} />;
}
2. Slate Implementation
Installation
npm install slate slate-react
Basic Setup
import React, { useCallback, useMemo } from 'react';
import { createEditor } from 'slate';
import { Slate, Editable, withReact } from 'slate-react';
export function SlateEditor() {
const editor = useMemo(() => withReact(createEditor()), []);
const [value, setValue] = React.useState([
{
type: 'paragraph',
children: [{ text: 'Start typing...' }],
},
]);
const renderElement = useCallback(props => {
switch (props.element.type) {
case 'heading':
return <h2 {...props.attributes}>{props.children}</h2>;
case 'quote':
return <blockquote {...props.attributes}>{props.children}</blockquote>;
default:
return <p {...props.attributes}>{props.children}</p>;
}
}, []);
const renderLeaf = useCallback(props => {
let { children } = props;
if (props.leaf.bold) {
children = <strong>{children}</strong>;
}
if (props.leaf.italic) {
children = <em>{children}</em>;
}
if (props.leaf.underline) {
children = <u>{children}</u>;
}
return <span {...props.attributes}>{children}</span>;
}, []);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<Editable
renderElement={renderElement}
renderLeaf={renderLeaf}
placeholder="Enter text..."
/>
</Slate>
);
}
Advanced Slate with Toolbar
import { Editor, Transforms } from 'slate';
import { useSlate } from 'slate-react';
const ToolbarButton = ({ format, icon }) => {
const editor = useSlate();
return (
<button
onMouseDown={event => {
event.preventDefault();
toggleMark(editor, format);
}}
>
{icon}
</button>
);
};
const toggleMark = (editor, format) => {
const isActive = isMarkActive(editor, format);
if (isActive) {
Editor.removeMark(editor, format);
} else {
Editor.addMark(editor, format, true);
}
};
const isMarkActive = (editor, format) => {
const marks = Editor.marks(editor);
return marks ? marks[format] === true : false;
};
export function AdvancedSlateEditor() {
const editor = useMemo(() => withReact(createEditor()), []);
const [value, setValue] = React.useState([
{ type: 'paragraph', children: [{ text: '' }] },
]);
return (
<Slate editor={editor} value={value} onChange={setValue}>
<div style={{ marginBottom: '10px' }}>
<ToolbarButton format="bold" icon="B" />
<ToolbarButton format="italic" icon="I" />
<ToolbarButton format="underline" icon="U" />
</div>
<Editable placeholder="Enter text..." />
</Slate>
);
}
3. Comparison Implementation
export function EditorComparison() {
const [editorType, setEditorType] = React.useState('quill');
return (
<div>
<div style={{ marginBottom: '20px' }}>
<button onClick={() => setEditorType('quill')}>Quill</button>
<button onClick={() => setEditorType('slate')}>Slate</button>
</div>
{editorType === 'quill' ? <QuillEditor /> : <SlateEditor />}
</div>
);
}
Tips & Best Practices
Quill Tips
Delta Format: Store as JSON for version control
const delta = editor.getContents(); // { ops: [{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }] }Performance: Debounce change events for large documents
const debouncedSave = debounce(() => saveContent(), 1000); quill.on('text-change', debouncedSave);Custom Modules: Extend functionality
class CustomModule { constructor(quill, options) { this.quill = quill; } } Quill.register('modules/custom', CustomModule);
Slate Tips
Immutability: Always use Transforms API
Transforms.setNodes(editor, { type: 'heading' });Plugins: Build reusable editor enhancements
const withPlugins = editor => { const { isInline, isVoid } = editor; editor.isInline = element => element.type === 'link' ? true : isInline(element); return editor; };Serialization: Convert to/from HTML
const serialize = node => { if (Text.isText(node)) return node.text; const children = node.children.map(n => serialize(n)).join(''); switch (node.type) { case 'heading': return `<h2>${children}</h2>`; default: return `<p>${children}</p>`; } };
General Best Practices
- Accessibility: Ensure keyboard navigation and screen reader support
- Mobile: Test touch interactions and responsive design
- Security: Sanitize HTML output to prevent XSS
- Testing: Mock editor instances in unit tests
- Bundle Size: Tree-shake unused features
Conclusion
Choose Quill for rapid development with standard requirements. Choose Slate for complex, custom workflows requiring fine-grained control. Both excel in their domains—evaluate based on project scope and team expertise.