Installation
Install via the shadcn registry (recommended):
$ pnpm dlx shadcn@latest add @editorcn/editorThis copies the component source directly into your project, so you can customize it freely.
Or install via npm/pnpm:
$ pnpm add @editorcn/editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-link @tiptap/extension-placeholderAfter installation, import the editor styles at the root of your application:
Copy // Your shadcn globals import "@editorcn/ui/globals.css"; // Editor styles import "@editorcn/editor/style.css";
If you installed via the shadcn registry, the package is copied into your project, so import from your local components path instead of @editorcn/editor throughout this page:
Copy import { RichTextEditor, Link } from "@/components/editor"; import "@/components/editor/style.css";
Tiptap editor
@editorcn/editor provides a UI layer for Tiptap. The RichTextEditor component works with the Editor instance of Tiptap. This means you have full control over the editor state and configuration via the useEditor hook.
The RichTextEditor component does not manage state for you — controls just execute operations on the Editor instance. For controlled mode or value transforms (HTML/Markdown conversion), refer to the tiptap.dev documentation.
Usage
Copy "use client"; import { useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import TextAlign from "@tiptap/extension-text-align"; import Placeholder from "@tiptap/extension-placeholder"; import { RichTextEditor, Link } from "@editorcn/editor"; const content = ` <h2 style="text-align: center;">Welcome to editorcn</h2> <p><code>RichTextEditor</code> is based on <a href="https://tiptap.dev/">Tiptap</a> and supports:</p> <ul> <li>Text formatting: <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <s>strikethrough</s></li> <li>Headings (h1-h6)</li> <li>Ordered and bullet lists</li> <li>Text alignment</li> </ul> `; function MyEditor() { const editor = useEditor({ immediatelyRender: false, extensions: [ StarterKit, Link, Underline, TextAlign.configure({ types: ["heading", "paragraph"] }), Placeholder.configure({ placeholder: "Start typing..." }), ], content, }); return ( <RichTextEditor editor={editor}> <RichTextEditor.Toolbar sticky> <RichTextEditor.ControlsGroup> <RichTextEditor.Bold /> <RichTextEditor.Italic /> <RichTextEditor.Underline /> <RichTextEditor.Strikethrough /> <RichTextEditor.Code /> <RichTextEditor.ClearFormatting /> </RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup> <RichTextEditor.H1 /> <RichTextEditor.H2 /> <RichTextEditor.H3 /> </RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup> <RichTextEditor.BulletList /> <RichTextEditor.OrderedList /> <RichTextEditor.Blockquote /> <RichTextEditor.Hr /> </RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup> <RichTextEditor.AlignLeft /> <RichTextEditor.AlignCenter /> <RichTextEditor.AlignRight /> </RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup> <RichTextEditor.Link /> <RichTextEditor.Unlink /> </RichTextEditor.ControlsGroup> <RichTextEditor.ControlsGroup> <RichTextEditor.Undo /> <RichTextEditor.Redo /> </RichTextEditor.ControlsGroup> </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor> ); }
RichTextEditor (Root)
The root wrapper that provides the editor context to all children.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | The Tiptap editor instance |
children | ReactNode | — | Toolbar, content, controls |
className | string | — | Additional CSS classes (merged via tailwind-merge) |
variant | "default" | "subtle" | "compact" | "default" | Visual variant |
labels | Partial<RichTextEditorLabels> | DEFAULT_LABELS | Override default labels for accessibility and UI text |
icons | Partial<RichTextEditorIcons> | DEFAULT_ICONS | Override default toolbar icons |
RichTextEditor.Toolbar
The toolbar that holds control groups. Supports sticky positioning.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
sticky | boolean | false | Makes toolbar stick to top on scroll |
stickyOffset | number | string | 0 | Offset from top when sticky (e.g. 60 or "var(--header-height)") |
className | string | — | Additional CSS classes |
Copy <RichTextEditor.Toolbar sticky stickyOffset={60}> {/* controls */} </RichTextEditor.Toolbar>
RichTextEditor.ControlsGroup
Groups related controls together. Automatically adds a visual separator between groups (a vertical divider line). No props other than className and children.
Copy <RichTextEditor.ControlsGroup> <RichTextEditor.Bold /> <RichTextEditor.Italic /> </RichTextEditor.ControlsGroup>
RichTextEditor.Content
Renders the Tiptap editor content area. Accepts an optional className prop.
Copy <RichTextEditor.Content />
RichTextEditor.Control
A generic control button for custom controls. Use built-in controls when possible.
Props
| Prop | Type | Description |
|---|---|---|
active | boolean | Whether the control is in active state |
interactive | boolean | Whether the control is interactive (default true) |
| Plus all standard button HTML attributes |
Basic custom control
Use the Control component with the useRichTextEditorContext hook to create controls that execute custom editor commands:
Copy import { useRichTextEditorContext } from "@editorcn/editor"; function InsertStarControl() { const { editor } = useRichTextEditorContext(); return ( <RichTextEditor.Control onClick={() => editor?.chain().focus().insertContent("⭐").run()} aria-label="Insert star emoji" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="rte-editor-icon" > <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /> </svg> </RichTextEditor.Control> ); }
Then use it in the toolbar like any built-in control:
Copy <RichTextEditor.Toolbar> <RichTextEditor.ControlsGroup> <RichTextEditor.Bold /> <RichTextEditor.Italic /> <InsertStarControl /> </RichTextEditor.ControlsGroup> </RichTextEditor.Toolbar>
Custom color highlight
Create a control that toggles a custom highlight color:
Copy import { useRichTextEditorContext } from "@editorcn/editor"; function YellowHighlightControl() { const { editor } = useRichTextEditorContext(); const isActive = editor?.isActive("highlight", { color: "#fef08a" }); return ( <RichTextEditor.Control active={isActive} onClick={() => editor?.chain().focus().toggleHighlight({ color: "#fef08a" }).run() } aria-label="Yellow highlight" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="rte-editor-icon" > <path d="m12 3-1.5 1.5" /> <path d="M12 21l1.5-1.5" /> <path d="M3 12l1.5-1.5" /> <path d="M21 12l-1.5 1.5" /> </svg> </RichTextEditor.Control> ); }
Custom link insert helper
Combine multiple commands into one control:
Copy function InsertExampleLink() { const { editor } = useRichTextEditorContext(); return ( <RichTextEditor.Control onClick={() => editor ?.chain() .focus() .insertContent("editorcn") .setLink({ href: "https://github.com/AbdullahMukadam/Editorcn" }) .run() } aria-label="Insert example link" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className="rte-editor-icon" > <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /> </svg> </RichTextEditor.Control> ); }
Controls and extensions
Some controls require additional Tiptap extensions to be installed and configured. If you installed via the shadcn registry, the CLI will prompt you to add any missing peer dependencies automatically; with npm/pnpm you'll need to add them yourself as shown below.
Included with @tiptap/starter-kit (no extra installation needed)
| Control | Tiptap Extension |
|---|---|
RichTextEditor.Bold | @tiptap/starter-kit |
RichTextEditor.Italic | @tiptap/starter-kit |
RichTextEditor.Strikethrough | @tiptap/starter-kit |
RichTextEditor.ClearFormatting | @tiptap/starter-kit |
RichTextEditor.Code | @tiptap/starter-kit |
RichTextEditor.CodeBlock | @tiptap/starter-kit |
RichTextEditor.H1–RichTextEditor.H6 | @tiptap/starter-kit |
RichTextEditor.BulletList | @tiptap/starter-kit |
RichTextEditor.OrderedList | @tiptap/starter-kit |
RichTextEditor.Blockquote | @tiptap/starter-kit |
RichTextEditor.Hr | @tiptap/starter-kit |
RichTextEditor.Undo | @tiptap/starter-kit |
RichTextEditor.Redo | @tiptap/starter-kit |
Controls requiring @tiptap/extension-underline
$ pnpm add @tiptap/extension-underline| Control |
|---|
RichTextEditor.Underline |
Controls requiring @tiptap/extension-text-align
$ pnpm add @tiptap/extension-text-alignCopy import TextAlign from "@tiptap/extension-text-align"; const editor = useEditor({ extensions: [TextAlign.configure({ types: ["heading", "paragraph"] })], });
| Control |
|---|
RichTextEditor.AlignLeft |
RichTextEditor.AlignCenter |
RichTextEditor.AlignRight |
RichTextEditor.AlignJustify |
Controls requiring @tiptap/extension-highlight
$ pnpm add @tiptap/extension-highlight| Control |
|---|
RichTextEditor.Highlight |
Controls requiring @tiptap/extension-subscript
$ pnpm add @tiptap/extension-subscript| Control |
|---|
RichTextEditor.Subscript |
Controls requiring @tiptap/extension-superscript
$ pnpm add @tiptap/extension-superscript| Control |
|---|
RichTextEditor.Superscript |
Link extension
@editorcn/editor ships a custom Link extension that extends @tiptap/extension-link. It is required for the Mod-K keyboard shortcut and the link popover UI to work correctly.
Copy import { Link, RichTextEditor } from "@editorcn/editor"; const editor = useEditor({ extensions: [ StarterKit, Link, // replaces @tiptap/extension-link ], }); // In toolbar: <RichTextEditor.ControlsGroup> <RichTextEditor.Link /> <RichTextEditor.Unlink /> </RichTextEditor.ControlsGroup>;
The link button opens a popover where users can enter or edit a URL. The Mod-K shortcut opens the link editor on selected text.
Embed extensions
@editorcn/editor ships with two embed extensions for embedding YouTube videos and tweets. They are self-contained (no extra Tiptap packages needed) and include resizable node views with drag handles.
YouTube embed
Embed YouTube videos by URL or video ID. Supports standard watch URLs, shorts, live streams, and youtu.be shortlinks.
Copy import { YouTubeEmbed, RichTextEditor } from "@editorcn/editor"; const editor = useEditor({ extensions: [ StarterKit, YouTubeEmbed, ], }); // In toolbar: <RichTextEditor.ControlsGroup> <RichTextEditor.YouTubeEmbed /> </RichTextEditor.ControlsGroup>;
The YouTube embed maintains a 16:9 aspect ratio and can be resized by dragging the handles that appear on selection. It is constrained to the editor width.
Twitter embed
Embed tweets by pasting a tweet URL or numeric ID. Supports both twitter.com and x.com URLs.
Copy import { TwitterEmbed, RichTextEditor } from "@editorcn/editor"; const editor = useEditor({ extensions: [ StarterKit, TwitterEmbed, ], }); // In toolbar: <RichTextEditor.ControlsGroup> <RichTextEditor.TwitterEmbed /> </RichTextEditor.ControlsGroup>;
The Twitter embed can be freely resized (no aspect ratio lock) and will re-render the tweet when dimensions change.
Both embeds
Add both to your editor:
Copy import { YouTubeEmbed, TwitterEmbed, RichTextEditor } from "@editorcn/editor"; const editor = useEditor({ extensions: [ StarterKit, YouTubeEmbed, TwitterEmbed, ], });
Both embeds support alignment (left, center, right) via the data-align attribute and can be resized by dragging the handles that appear when selected.
Placeholder
Install @tiptap/extension-placeholder to show placeholder text when the editor is empty:
$ pnpm add @tiptap/extension-placeholderCopy import Placeholder from "@tiptap/extension-placeholder"; const editor = useEditor({ extensions: [ StarterKit, Placeholder.configure({ placeholder: "Start typing..." }), ], content: "", });
The placeholder is styled using the --muted-foreground CSS variable and adapts to your theme.
Controlled mode
To control the editor state, use the onUpdate callback on the useEditor hook:
Copy import { useEditor } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { RichTextEditor } from "@editorcn/editor"; interface Props { value: string; onChange: (value: string) => void; } function MyControlledEditor({ value, onChange }: Props) { const editor = useEditor({ extensions: [StarterKit], content: value, onUpdate: ({ editor }) => { onChange(editor.getHTML()); }, }); return ( <RichTextEditor editor={editor}> <RichTextEditor.Toolbar> <RichTextEditor.ControlsGroup> <RichTextEditor.Bold /> <RichTextEditor.Italic /> </RichTextEditor.ControlsGroup> </RichTextEditor.Toolbar> <RichTextEditor.Content /> </RichTextEditor> ); }
Labels and localization
Override labels for all controls with the labels prop. Labels are used for aria-label and title attributes.
Copy import { RichTextEditor, DEFAULT_LABELS } from "@editorcn/editor"; <RichTextEditor editor={editor} labels={{ boldControlLabel: "Gras", italicControlLabel: "Kursiv", ...DEFAULT_LABELS, }} > {/* ... */} </RichTextEditor>;
All available labels
Copy interface RichTextEditorLabels { // Controls boldControlLabel: string; italicControlLabel: string; underlineControlLabel: string; strikeControlLabel: string; clearFormattingControlLabel: string; codeControlLabel: string; codeBlockControlLabel: string; h1ControlLabel: string; h2ControlLabel: string; h3ControlLabel: string; h4ControlLabel: string; h5ControlLabel: string; h6ControlLabel: string; bulletListControlLabel: string; orderedListControlLabel: string; blockquoteControlLabel: string; hrControlLabel: string; linkControlLabel: string; unlinkControlLabel: string; undoControlLabel: string; redoControlLabel: string; alignLeftControlLabel: string; alignCenterControlLabel: string; alignRightControlLabel: string; alignJustifyControlLabel: string; highlightControlLabel: string; subscriptControlLabel: string; superscriptControlLabel: string; tasksControlLabel: string; tasksSinkLabel: string; tasksLiftLabel: string; sourceCodeControlLabel: string; // Link editor linkEditorInputLabel: string; linkEditorInputPlaceholder: string; linkEditorExternalLink: string; linkEditorInternalLink: string; linkEditorSave: string; }
Default labels
Copy import { DEFAULT_LABELS } from "@editorcn/editor"; // Values: { boldControlLabel: "Bold", italicControlLabel: "Italic", underlineControlLabel: "Underline", strikeControlLabel: "Strikethrough", clearFormattingControlLabel: "Clear formatting", codeControlLabel: "Code", codeBlockControlLabel: "Code block", h1ControlLabel: "Heading 1", h2ControlLabel: "Heading 2", h3ControlLabel: "Heading 3", h4ControlLabel: "Heading 4", h5ControlLabel: "Heading 5", h6ControlLabel: "Heading 6", bulletListControlLabel: "Bullet list", orderedListControlLabel: "Ordered list", blockquoteControlLabel: "Blockquote", hrControlLabel: "Horizontal rule", linkControlLabel: "Link", unlinkControlLabel: "Remove link", undoControlLabel: "Undo", redoControlLabel: "Redo", alignLeftControlLabel: "Align left", alignCenterControlLabel: "Align center", alignRightControlLabel: "Align right", alignJustifyControlLabel: "Align justify", highlightControlLabel: "Highlight", subscriptControlLabel: "Subscript", superscriptControlLabel: "Superscript", tasksControlLabel: "Task list", tasksSinkLabel: "Decrease task level", tasksLiftLabel: "Increase task level", sourceCodeControlLabel: "Source code", linkEditorInputLabel: "Enter URL", linkEditorInputPlaceholder: "https://example.com", linkEditorExternalLink: "Open in new tab", linkEditorInternalLink: "Open in same tab", linkEditorSave: "Save", }
Icons
Override any toolbar icon with the icons prop. Icons are rendered inside control buttons and use the .rte-editor-icon class for sizing.
Copy import { RichTextEditor, DEFAULT_ICONS } from "@editorcn/editor"; <RichTextEditor editor={editor} icons={{ boldControlIcon: <YourBoldIcon />, italicControlIcon: <YourItalicIcon />, }} > {/* ... */} </RichTextEditor>;
All available icons
Copy interface RichTextEditorIcons { boldControlIcon: React.ReactNode; italicControlIcon: React.ReactNode; underlineControlIcon: React.ReactNode; strikeControlIcon: React.ReactNode; clearFormattingControlIcon: React.ReactNode; codeControlIcon: React.ReactNode; codeBlockControlIcon: React.ReactNode; h1ControlIcon: React.ReactNode; h2ControlIcon: React.ReactNode; h3ControlIcon: React.ReactNode; h4ControlIcon: React.ReactNode; h5ControlIcon: React.ReactNode; h6ControlIcon: React.ReactNode; bulletListControlIcon: React.ReactNode; orderedListControlIcon: React.ReactNode; blockquoteControlIcon: React.ReactNode; hrControlIcon: React.ReactNode; linkControlIcon: React.ReactNode; unlinkControlIcon: React.ReactNode; undoControlIcon: React.ReactNode; redoControlIcon: React.ReactNode; alignLeftControlIcon: React.ReactNode; alignCenterControlIcon: React.ReactNode; alignRightControlIcon: React.ReactNode; alignJustifyControlIcon: React.ReactNode; highlightControlIcon: React.ReactNode; subscriptControlIcon: React.ReactNode; superscriptControlIcon: React.ReactNode; }
Styling custom icons
Apply the rte-editor-icon class to your custom icons so they inherit the correct toolbar dimensions:
Copy <RichTextEditor.Control onClick={() => editor?.chain().focus().insertContent("...").run()} aria-label="Custom" > <svg className="rte-editor-icon" /* ... */>{/* paths */}</svg> </RichTextEditor.Control>
The default icons are inline SVGs with strokeWidth={2}, 24×24 viewBox, and rte-editor-icon class. See the Styling guide for size overrides.
Editor context
Use the useRichTextEditorContext hook to access the editor instance from inside a child component:
Copy import { useRichTextEditorContext } from "@editorcn/editor"; function CustomBoldButton() { const { editor } = useRichTextEditorContext(); return ( <button onClick={() => editor?.chain().focus().toggleBold().run()}> Bold </button> ); }
Code highlight
To use syntax highlighting in code blocks, install @tiptap/extension-code-block-lowlight and lowlight:
$ pnpm add lowlight @tiptap/extension-code-block-lowlightCopy import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { createLowlight } from "lowlight"; import ts from "highlight.js/lib/languages/typescript"; const lowlight = createLowlight(); lowlight.register({ ts }); const editor = useEditor({ extensions: [ StarterKit.configure({ codeBlock: false }), CodeBlockLowlight.configure({ lowlight }), ], });
The editor styles include syntax highlighting tokens for .hljs-* classes that use your theme's CSS variables (--primary, --muted-foreground, --destructive, --accent-foreground).
Styling
Both the editor UI and content area are styled using CSS variables from your shadcn theme. See the Styling guide for details on customization.
On This Page
InstallationTiptap editorUsageRichTextEditor (Root)PropsRichTextEditor.ToolbarPropsRichTextEditor.ControlsGroupRichTextEditor.ContentRichTextEditor.ControlPropsBasic custom controlCustom color highlightCustom link insert helperControls and extensionsIncluded with@tiptap/starter-kit (no extra installation needed)Controls requiring @tiptap/extension-underlineControls requiring @tiptap/extension-text-alignControls requiring @tiptap/extension-highlightControls requiring @tiptap/extension-subscriptControls requiring @tiptap/extension-superscriptLink extensionEmbed extensionsYouTube embedTwitter embedBoth embedsPlaceholderControlled modeLabels and localizationAll available labelsDefault labelsIconsAll available iconsStyling custom iconsEditor contextCode highlightStyling