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:

  1. ContentEditable understanding and its limitations
  2. State management for complex editor state
  3. Performance optimization for large documents
  4. 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.