Tools

Editor

A feature-rich WYSIWYG text editor component with formatting tools, undo/redo support, font selection, and live preview mode.

Last updated on

A professional, fully-featured rich text editor built with React. Includes comprehensive formatting capabilities, keyboard shortcuts, font selection, real-time preview mode, and intuitive toolbar controls.

Installation

npx shadcn@latest add @grenish/editor

This requires the @grenish registry in your components.json. See the installation guide for setup.

Manual Dependencies

If you prefer manual installation, add the base components:

npx shadcn@latest add card button select toggle-group tooltip input-group dialog

Usage

import MainEditor from "@/components/tools/editor"

export default function EditorDemo() {
  return <MainEditor />
}
"use client";

import { Card, CardFooter } from "@/components/ui/card";
import EditorToolbar from "./toolbar";
import EditorBody from "./editor-body";
import { Button } from "@/components/ui/button";
import { useEditorState } from "./hooks/useEditorState";
import { useTextFormatting } from "./hooks/useTextFormatting";
import { useState } from "react";
import DeleteButton from "../delete-button";

export default function MainEditor() {
  const editor = useEditorState();
  const formatting = useTextFormatting(editor.editorRef);
  const [isPreviewMode, setIsPreviewMode] = useState(false);

  const togglePreviewMode = () => {
    setIsPreviewMode(!isPreviewMode);
  };

  return (
    <Card className="w-full">
      <EditorToolbar
        editorRef={editor.editorRef}
        fontFamily={editor.fontFamily}
        setFont={editor.setFont}
        canUndo={editor.canUndo}
        canRedo={editor.canRedo}
        onUndo={editor.undo}
        onRedo={editor.redo}
        onBold={() => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
          }
          editor.toggleFormat("bold");
        }}
        onItalic={() => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
          }
          editor.toggleFormat("italic");
        }}
        onUnderline={() => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
          }
          editor.toggleFormat("underline");
        }}
        onHeading={(heading: "h1" | "h2") => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
          }
          editor.applyFormat(heading);
        }}
        onBulletList={() => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
            document.execCommand("insertUnorderedList");
            editor.updateContent(editor.editorRef.current.innerHTML);
          }
        }}
        onNumberList={() => {
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
            document.execCommand("insertOrderedList");
            editor.updateContent(editor.editorRef.current.innerHTML);
          }
        }}
        onInsertLink={() => {
          // LinkPrompt handles the link insertion, we just need to update content
          if (editor.editorRef.current) {
            editor.editorRef.current.focus();
            editor.updateContent(editor.editorRef.current.innerHTML);
          }
        }}
        onInsertImage={() => {
          const url = prompt("Enter the image URL:");
          if (url && editor.editorRef.current) {
            editor.editorRef.current.focus();
            document.execCommand("insertImage", false, url);
            editor.updateContent(editor.editorRef.current.innerHTML);
          }
        }}
        activeFormats={formatting.activeFormats}
        isPreviewMode={isPreviewMode}
        onTogglePreview={togglePreviewMode}
      />
      <EditorBody
        editorRef={editor.editorRef}
        fontFamily={editor.fontFamily}
        onInput={editor.handleInput}
        onKeyDown={editor.handleKeyDown}
        onUpdateFormats={formatting.updateActiveFormats}
        isPreviewMode={isPreviewMode}
      />
      <CardFooter className="gap-1">
        <Button size={"sm"}>Save</Button>
        <DeleteButton size={"sm"} />
      </CardFooter>
    </Card>
  );
}

File Structure

editor-body.tsx
main-editor.tsx
toolbar.tsx

Source Code

Explorer
editor/main-editor.tsx
1
"use client";
2
3
import { Card, CardFooter } from "@/components/ui/card";
4
import EditorToolbar from "./toolbar";
5
import EditorBody from "./editor-body";
6
import { Button } from "@/components/ui/button";
7
import { useEditorState } from "./hooks/useEditorState";
8
import { useTextFormatting } from "./hooks/useTextFormatting";
9
import { useState } from "react";
10
import DeleteButton from "../delete-button";
11
12
export default function MainEditor() {
13
const editor = useEditorState();
14
const formatting = useTextFormatting(editor.editorRef);
15
const [isPreviewMode, setIsPreviewMode] = useState(false);
16
17
const togglePreviewMode = () => {
18
setIsPreviewMode(!isPreviewMode);
19
};
20
21
return (
22
<Card className="w-full">
23
<EditorToolbar
24
editorRef={editor.editorRef}
25
fontFamily={editor.fontFamily}
26
setFont={editor.setFont}
27
canUndo={editor.canUndo}
28
canRedo={editor.canRedo}
29
onUndo={editor.undo}
30
onRedo={editor.redo}
31
onBold={() => {
32
if (editor.editorRef.current) {
33
editor.editorRef.current.focus();
34
}
35
editor.toggleFormat("bold");
36
}}
37
onItalic={() => {
38
if (editor.editorRef.current) {
39
editor.editorRef.current.focus();
40
}
41
editor.toggleFormat("italic");
42
}}
43
onUnderline={() => {
44
if (editor.editorRef.current) {
45
editor.editorRef.current.focus();
46
}
47
editor.toggleFormat("underline");
48
}}
49
onHeading={(heading: "h1" | "h2") => {
50
if (editor.editorRef.current) {
51
editor.editorRef.current.focus();
52
}
53
editor.applyFormat(heading);
54
}}
55
onBulletList={() => {
56
if (editor.editorRef.current) {
57
editor.editorRef.current.focus();
58
document.execCommand("insertUnorderedList");
59
editor.updateContent(editor.editorRef.current.innerHTML);
60
}
61
}}
62
onNumberList={() => {
63
if (editor.editorRef.current) {
64
editor.editorRef.current.focus();
65
document.execCommand("insertOrderedList");
66
editor.updateContent(editor.editorRef.current.innerHTML);
67
}
68
}}
69
onInsertLink={() => {
70
if (editor.editorRef.current) {
71
editor.editorRef.current.focus();
72
editor.updateContent(editor.editorRef.current.innerHTML);
73
}
74
}}
75
onInsertImage={() => {
76
const url = prompt("Enter the image URL:");
77
if (url && editor.editorRef.current) {
78
editor.editorRef.current.focus();
79
document.execCommand("insertImage", false, url);
80
editor.updateContent(editor.editorRef.current.innerHTML);
81
}
82
}}
83
activeFormats={formatting.activeFormats}
84
isPreviewMode={isPreviewMode}
85
onTogglePreview={togglePreviewMode}
86
/>
87
<EditorBody
88
editorRef={editor.editorRef}
89
fontFamily={editor.fontFamily}
90
onInput={editor.handleInput}
91
onKeyDown={editor.handleKeyDown}
92
onUpdateFormats={formatting.updateActiveFormats}
93
isPreviewMode={isPreviewMode}
94
/>
95
<CardFooter className="gap-1">
96
<Button size={"sm"}>Save</Button>
97
<DeleteButton size={"sm"} />
98
</CardFooter>
99
</Card>
100
);
101
}

Features

  • Rich Text Editing: Support for bold, italic, underline, headings, and list formatting
  • Multiple Font Selection: Choose from 10 different font families including Roboto, Montserrat, Lato, and more
  • Undo/Redo Support: Full history support with customizable history size (up to 50 actions)
  • Live Preview Mode: Toggle between editing and preview modes to see your content as it will appear
  • Keyboard Shortcuts:
    • Ctrl+B / Cmd+B: Bold
    • Ctrl+I / Cmd+I: Italic
    • Ctrl+U / Cmd+U: Underline
    • Ctrl+Z / Cmd+Z: Undo
    • Ctrl+Y / Cmd+Y or Ctrl+Shift+Z: Redo
  • Link & Image Insertion: Easy-to-use dialogs for adding links and images
  • Text Formatting Toolbar: Intuitive toolbar with grouped controls for quick access to formatting options
  • Empty State Handling: Placeholder text and empty state detection
  • Responsive Design: Works seamlessly on different screen sizes

On this page