Skip to main content

Command Palette

Search for a command to run...

Rich Text Editor: WYSIWYG Implementation

Learn: Rich Text Editor: WYSIWYG Implementation

Updated
5 min readView as Markdown
T

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

  1. Delta Format: Store as JSON for version control

    const delta = editor.getContents();
    // { ops: [{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }] }
    
  2. Performance: Debounce change events for large documents

    const debouncedSave = debounce(() => saveContent(), 1000);
    quill.on('text-change', debouncedSave);
    
  3. Custom Modules: Extend functionality

    class CustomModule {
     constructor(quill, options) {
       this.quill = quill;
     }
    }
    Quill.register('modules/custom', CustomModule);
    

Slate Tips

  1. Immutability: Always use Transforms API

    Transforms.setNodes(editor, { type: 'heading' });
    
  2. Plugins: Build reusable editor enhancements

    const withPlugins = editor => {
      const { isInline, isVoid } = editor;
      editor.isInline = element => 
        element.type === 'link' ? true : isInline(element);
      return editor;
    };
    
  3. 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.