Installation
Install via the shadcn registry (recommended):
$ pnpm dlx shadcn@latest add @editorcn/editor
This 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-placeholder
After installation, import the editor styles at the root of your application:
// Your shadcn globals
import "@/app/globals.css";
// Editor component styles
import "@/components/editor/style.css";Order matters: Import your shadcn globals first, then the editor styles.
If you installed via npm, import from @editorcn/editor instead of @/components/editor throughout this page:
import { RichTextEditor, Link } from "@/components/editor";
import "@editorcn/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
"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 "@/components/editor";
import "@/components/editor/style.css";
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 |
<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.
<RichTextEditor.ControlsGroup>
<RichTextEditor.Bold />
<RichTextEditor.Italic />
</RichTextEditor.ControlsGroup>RichTextEditor.Content
Renders the Tiptap editor content area. Accepts an optional className prop.
<RichTextEditor.Content />RichTextEditor.Footer
A footer bar that sits below the content area. Useful for displaying word count, character count, or custom actions.
To use the word count feature, you must add the @tiptap/extension-character-count extension to your editor.
$ pnpm add @tiptap/extension-character-count
Props
| Prop | Type | Default | Description |
|---|---|---|---|
showWordCount | boolean | false | Show word and character count (requires CharacterCount) |
sticky | boolean | false | Makes footer stick to bottom on scroll |
stickyOffset | number | string | 0 | Offset from bottom when sticky |
wordCountClassName | string | — | Additional CSS classes for the word count element |
wordCountFormatter | (info: { words, characters }) => string | — | Custom formatter for the word count text |
className | string | — | Additional CSS classes |
children | ReactNode | — | Custom content to render in the footer |
Basic usage
import { CharacterCount } from "@tiptap/extension-character-count";
const editor = useEditor({
extensions: [
StarterKit,
CharacterCount, // required for word count
],
});
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar>...</RichTextEditor.Toolbar>
<RichTextEditor.Content />
<RichTextEditor.Footer showWordCount />
</RichTextEditor>;Custom formatter
<RichTextEditor.Footer
showWordCount
wordCountFormatter={({ words, characters }) =>
`${words} words · ${characters} chars`
}
/>With custom content
<RichTextEditor.Footer>
<span>Custom footer content</span>
</RichTextEditor.Footer>Sticky footer
<RichTextEditor.Footer sticky stickyOffset={0} showWordCount />RichTextEditor.BubbleMenu
A floating formatting menu that appears when you select text inside the content area. It shows inline formatting actions (bold, italic, underline, strikethrough, code); when the selection is inside a code block it instead shows the syntax language selector.
<RichTextEditor editor={editor}>
<RichTextEditor.Toolbar>...</RichTextEditor.Toolbar>
<RichTextEditor.BubbleMenu editor={editor} />
<RichTextEditor.Content />
</RichTextEditor>Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | The Tiptap editor instance |
The bubble menu uses Floating UI via @tiptap/react/menus and hides automatically when the editor becomes read-only (editor.setEditable(false)). Its visuals follow your shadcn theme tokens and can be customized with the .rte-bubble-menu, .rte-bubble-btn, and .rte-bubble-dropdown classes.
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:
import { useRichTextEditorContext } from "@/components/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:
<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:
import { useRichTextEditorContext } from "@/components/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:
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-align
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.
import { Link, RichTextEditor } from "@/components/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.
import { YouTubeEmbed, RichTextEditor } from "@/components/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.
import { TwitterEmbed, RichTextEditor } from "@/components/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:
import {
YouTubeEmbed,
TwitterEmbed,
RichTextEditor,
} from "@/components/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-placeholder
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:
import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { RichTextEditor } from "@/components/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.
import { RichTextEditor, DEFAULT_LABELS } from "@/components/editor";
<RichTextEditor
editor={editor}
labels={{
boldControlLabel: "Gras",
italicControlLabel: "Kursiv",
...DEFAULT_LABELS,
}}
>
{/* ... */}
</RichTextEditor>;All available labels
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
import { DEFAULT_LABELS } from "@/components/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.
import { RichTextEditor, DEFAULT_ICONS } from "@/components/editor";
<RichTextEditor
editor={editor}
icons={{
boldControlIcon: <YourBoldIcon />,
italicControlIcon: <YourItalicIcon />,
}}
>
{/* ... */}
</RichTextEditor>;Language icons
The code block language selector shows per-language icons (e.g. a JavaScript icon for JS, a Python icon for Python). These are provided via the languageIcons key in the icons object:
import {
RichTextEditor,
DEFAULT_ICONS,
DEFAULT_LANGUAGE_ICONS,
} from "@/components/editor";
<RichTextEditor
editor={editor}
icons={{
...DEFAULT_ICONS,
languageIcons: {
...DEFAULT_LANGUAGE_ICONS,
jsx: <YourJsxIcon />, // override a specific language icon
},
}}
>
{/* ... */}
</RichTextEditor>;Exported constants:
| Export | Description |
|---|---|
DEFAULT_ICONS | Default toolbar icons |
DEFAULT_LANGUAGE_ICONS | Per-language icons for 21 languages (js, ts, python, rust, go, html, css, etc.) |
All available icons
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;
languageIcons: Record<string, React.ReactNode>; // per-language icons for code block language selector
}Styling custom icons
Apply the rte-editor-icon class to your custom icons so they inherit the correct toolbar dimensions:
<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:
import { useRichTextEditorContext } from "@/components/editor";
function CustomBoldButton() {
const { editor } = useRichTextEditorContext();
return (
<button onClick={() => editor?.chain().focus().toggleBold().run()}>
Bold
</button>
);
}Code highlight
@editorcn/editor ships with @tiptap/extension-code-block-lowlight and lowlight pre-configured. Code blocks automatically get syntax highlighting with 21 built-in languages — no extra setup needed.
If you want to add additional languages, install the highlight.js language modules and register them:
$ pnpm add lowlight
import { createLowlight, common } from "lowlight";
import python from "highlight.js/lib/languages/python";
const lowlight = createLowlight(common);
lowlight.register({ python });
// Pass to CodeBlock extension in your editor config
CodeBlock.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.FooterPropsBasic usageCustom formatterWith custom contentSticky footerRichTextEditor.BubbleMenuPropsRichTextEditor.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 labelsIconsLanguage iconsAll available iconsStyling custom iconsEditor contextCode highlightStyling