Questions
Design Rich Text Editor (WYSIWYG)
Step-by-step guide to building a rich text editor using the browser's native `contentEditable` feature with real-time collaboration and modern React patterns.
Design Rich Text Editor (WYSIWYG)
Learn how to build a powerful, scalable WYSIWYG editor with undo/redo functionality, real-time collaboration, and modern React patterns.
Problem Statement
Design and implement a WYSIWYG (What You See Is What You Get) rich text editor that supports:
- Real-time text editing with formatting options (bold, italic, underline, etc.)
- Undo/Redo functionality with proper state management
- Collaborative editing capabilities with conflict resolution
- Custom toolbar with formatting controls and advanced features
- Performance optimization for large documents
- Accessibility compliance for all users
- Mobile responsiveness with touch support
- Content export in multiple formats (HTML, Markdown, PDF)
Clarifying Questions
Functional Requirements
- Document Size: How large should documents be supported (words, pages)?
- Collaboration: How many users need to edit simultaneously?
- Formatting Options: Which formatting features are required (text, images, tables, etc.)?
- Content Types: Should it support rich media (images, videos, embeds)?
- Export Formats: What output formats are needed (HTML, Markdown, PDF)?
- Version Control: Is document history and versioning required?
Technical Constraints
- Performance: What's the acceptable response time for collaborative edits?
- Browser Support: Which browsers and versions need to be supported?
- Mobile Support: Should it work on touch devices with gestures?
- Offline Support: Should it work without internet connection?
- Security: What level of content sanitization is required?
High-Level Architecture
1. Component Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Editor Core │ │ State Manager │ │ Real-time │
│ (ContentEditable)│◄──►│ (Redux/Zustand)│◄──►│ Sync (WebSocket)│
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Toolbar UI │ │ History │ │ Content │
│ (Formatting) │ │ (Undo/Redo) │ │ Parser │
└─────────────────┘ └─────────────────┘ └─────────────────┘2. Core Components
Editor Core
- ContentEditable div as the main editing surface
- Event handling for user interactions and content changes
- Selection management and cursor positioning
- Content sanitization and validation
Toolbar Component
- Formatting buttons (bold, italic, underline, etc.)
- Advanced formatting (headings, lists, alignment)
- Media insertion (images, videos, embeds)
- Export options and settings
History Manager
- Undo/Redo stack implementation
- State snapshot management
- Performance optimization for large histories
- Memory management and cleanup
Collaboration Layer
- WebSocket connection for real-time updates
- Operational transformation for conflict resolution
- Cursor synchronization across multiple users
- Presence indicators and user avatars
Undo/Redo Implementation
Core Concept: State History Management
The undo/redo system tracks the editor's content state over time, allowing users to navigate through their editing history:
interface EditorState {
content: string;
selection?: {
start: number;
end: number;
direction?: 'forward' | 'backward' | 'none';
};
timestamp: number;
}
class EditorHistory {
private past: EditorState[] = [];
private present: EditorState;
private future: EditorState[] = [];
private maxSize: number;
constructor(initialState: EditorState, maxSize: number = 50) {
this.present = initialState;
this.maxSize = maxSize;
}
push(newState: EditorState): void {
this.past.push(this.present);
this.present = newState;
this.future = []; // Clear future when new state is pushed
// Maintain history size limit
if (this.past.length > this.maxSize) {
this.past.shift();
}
}
undo(): EditorState | null {
if (this.past.length === 0) return null;
const previous = this.past.pop()!;
this.future.unshift(this.present);
this.present = previous;
return this.present;
}
redo(): EditorState | null {
if (this.future.length === 0) return null;
const next = this.future.shift()!;
this.past.push(this.present);
this.present = next;
return this.present;
}
canUndo(): boolean {
return this.past.length > 0;
}
canRedo(): boolean {
return this.future.length > 0;
}
}React Hook Implementation
const useEditorHistory = (initialContent: string = '') => {
const [history, setHistory] = useState(() =>
new EditorHistory({ content: initialContent, timestamp: Date.now() })
);
const pushState = useCallback((newContent: string, selection?: Selection) => {
setHistory(prev => {
const newHistory = new EditorHistory(prev.getCurrentState(), prev.maxSize);
newHistory.past = [...prev.past];
newHistory.future = [...prev.future];
newHistory.push({
content: newContent,
selection,
timestamp: Date.now(),
});
return newHistory;
});
}, []);
const undo = useCallback(() => {
setHistory(prev => {
const newHistory = new EditorHistory(prev.getCurrentState(), prev.maxSize);
newHistory.past = [...prev.past];
newHistory.future = [...prev.future];
const undoneState = newHistory.undo();
return undoneState ? newHistory : prev;
});
}, []);
const redo = useCallback(() => {
setHistory(prev => {
const newHistory = new EditorHistory(prev.getCurrentState(), prev.maxSize);
newHistory.past = [...prev.past];
newHistory.future = [...prev.future];
const redoneState = newHistory.redo();
return redoneState ? newHistory : prev;
});
}, []);
return {
currentState: history.getCurrentState(),
pushState,
undo,
redo,
canUndo: history.canUndo(),
canRedo: history.canRedo(),
};
};Editor Core Implementation
ContentEditable Component
interface EditorProps {
initialContent?: string;
placeholder?: string;
onChange?: (content: string) => void;
onSelectionChange?: (selection: Selection) => void;
readOnly?: boolean;
className?: string;
}
const RichTextEditor: React.FC<EditorProps> = ({
initialContent = '',
placeholder = 'Start typing...',
onChange,
onSelectionChange,
readOnly = false,
className,
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const { currentState, pushState, undo, redo, canUndo, canRedo } = useEditorHistory(initialContent);
const [isComposing, setIsComposing] = useState(false);
// Debounced content change handler
const debouncedOnChange = useDebounce((content: string) => {
onChange?.(content);
}, 300);
const handleInput = useCallback(() => {
if (!editorRef.current || isComposing) return;
const content = editorRef.current.innerHTML;
const selection = getCurrentSelection();
pushState(content, selection);
debouncedOnChange(content);
}, [pushState, debouncedOnChange, isComposing]);
const handleSelectionChange = useCallback(() => {
if (!editorRef.current) return;
const selection = getCurrentSelection();
onSelectionChange?.(selection);
}, [onSelectionChange]);
// Keyboard shortcuts
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.metaKey || e.ctrlKey) {
switch (e.key) {
case 'z':
e.preventDefault();
if (e.shiftKey) {
redo();
} else {
undo();
}
break;
case 'y':
e.preventDefault();
redo();
break;
case 'b':
e.preventDefault();
document.execCommand('bold');
break;
case 'i':
e.preventDefault();
document.execCommand('italic');
break;
case 'u':
e.preventDefault();
document.execCommand('underline');
break;
}
}
}, [undo, redo]);
return (
<div className={`rich-text-editor ${className || ''}`}>
<div
ref={editorRef}
contentEditable={!readOnly}
suppressContentEditableWarning
onInput={handleInput}
onSelect={handleSelectionChange}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="editor-content"
role="textbox"
aria-multiline="true"
aria-label="Rich text editor"
/>
</div>
);
};Toolbar Implementation
Formatting Toolbar Component
interface ToolbarProps {
onFormat: (command: string, value?: string) => void;
onUndo: () => void;
onRedo: () => void;
canUndo: boolean;
canRedo: boolean;
currentFormats: Record<string, boolean>;
}
const FormattingToolbar: React.FC<ToolbarProps> = ({
onFormat,
onUndo,
onRedo,
canUndo,
canRedo,
currentFormats,
}) => {
const formatButtons = [
{ command: 'bold', icon: 'B', label: 'Bold' },
{ command: 'italic', icon: 'I', label: 'Italic' },
{ command: 'underline', icon: 'U', label: 'Underline' },
{ command: 'strikeThrough', icon: 'S', label: 'Strikethrough' },
];
const alignmentButtons = [
{ command: 'justifyLeft', icon: '⫷', label: 'Align Left' },
{ command: 'justifyCenter', icon: '⫸', label: 'Align Center' },
{ command: 'justifyRight', icon: '⫹', label: 'Align Right' },
{ command: 'justifyFull', icon: '⫺', label: 'Justify' },
];
const handleFormat = (command: string, value?: string) => {
document.execCommand(command, false, value);
onFormat(command, value);
};
return (
<div className="formatting-toolbar">
{/* History Controls */}
<div className="toolbar-group">
<button
onClick={onUndo}
disabled={!canUndo}
className="toolbar-button"
title="Undo (Ctrl+Z)"
>
↶
</button>
<button
onClick={onRedo}
disabled={!canRedo}
className="toolbar-button"
title="Redo (Ctrl+Y)"
>
↷
</button>
</div>
{/* Text Formatting */}
<div className="toolbar-group">
{formatButtons.map(({ command, icon, label }) => (
<button
key={command}
onClick={() => handleFormat(command)}
className={`toolbar-button ${currentFormats[command] ? 'active' : ''}`}
title={label}
>
{icon}
</button>
))}
</div>
{/* Text Alignment */}
<div className="toolbar-group">
{alignmentButtons.map(({ command, icon, label }) => (
<button
key={command}
onClick={() => handleFormat(command)}
className={`toolbar-button ${currentFormats[command] ? 'active' : ''}`}
title={label}
>
{icon}
</button>
))}
</div>
</div>
);
};Real-Time Collaboration
Operational Transformation Implementation
interface Operation {
type: 'insert' | 'delete';
position: number;
content?: string;
length?: number;
timestamp: number;
userId: string;
}
class OperationalTransformation {
private operations: Operation[] = [];
private version: number = 0;
applyOperation(operation: Operation, content: string): string {
let newContent = content;
switch (operation.type) {
case 'insert':
newContent = content.slice(0, operation.position) +
(operation.content || '') +
content.slice(operation.position);
break;
case 'delete':
newContent = content.slice(0, operation.position) +
content.slice(operation.position + (operation.length || 0));
break;
}
this.operations.push(operation);
this.version++;
return newContent;
}
transformOperation(operation: Operation, appliedOperations: Operation[]): Operation {
let transformedOperation = { ...operation };
for (const appliedOp of appliedOperations) {
if (appliedOp.timestamp >= operation.timestamp) continue;
transformedOperation = this.transform(transformedOperation, appliedOp);
}
return transformedOperation;
}
private transform(op1: Operation, op2: Operation): Operation {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position <= op2.position) {
return op1;
} else {
return { ...op1, position: op1.position + (op2.content?.length || 0) };
}
}
// Additional transformation logic for other operation combinations
return op1;
}
}Best Practices
Performance
- Debounced updates to reduce unnecessary re-renders
- Virtual rendering for large documents
- Memory management for undo/redo history
- Lazy loading for media content
User Experience
- Keyboard shortcuts for power users
- Visual feedback for all formatting actions
- Auto-save functionality
- Error recovery and state restoration
Accessibility
- Keyboard navigation support
- Screen reader compatibility
- ARIA labels and roles
- Focus management for toolbar
Security
- Content sanitization to prevent XSS
- Input validation for user content
- Secure WebSocket connections
- Rate limiting for collaborative edits
Interview Tips
What Interviewers Look For:
- ContentEditable understanding and its limitations
- State management for complex editor state
- Performance optimization for large documents
- Collaborative editing implementation
Sample Questions:
- "How would you handle 100,000 words efficiently?"
- "What if two users edit the same paragraph simultaneously?"
- "How would you implement spell-checking?"
- "How would you optimize for mobile devices?"
Key Points to Mention:
- Use ContentEditable as the foundation
- Implement operational transformation for collaboration
- Plan for performance with virtual rendering
- Consider accessibility from the beginning
- Handle edge cases in contentEditable
Next: Google Search Design - Learn how to build a scalable autocomplete system.