Editorcn: React Rich Text Editors for shadcn/ui

Install customizable Tiptap toolbar or Notion-style block editors into your React project using the shadcn CLI and Tailwind CSS tokens.

editorcn is a React rich text editor that creates toolbar-style and Notion-style Tiptap editors for shadcn/ui projects.

It works with React, Tailwind CSS, and Tiptap, with a shadcn registry option that copies editable editor source into the project.

Features

  • Two editing models for toolbar and block-based UI.
  • 20+ toolbar controls for text formatting, headings, lists, links, alignment, embeds, and history.
  • Slash commands, block dragging, block actions, and inline formatting.
  • Editable local source through the shadcn registry.
  • shadcn/ui CSS variables for colors, typography, radius, light mode, and dark mode.
  • HTML, JSON, and plain-text serialization through Tiptap.
  • Static rendering for saved editor HTML.

Toolbar Editor or Block Editor?

Editing ModelPackageMain UI
Traditional rich text editor@editorcn/editorTop toolbar with grouped formatting controls
Block editor@editorcn/block-editorSlash commands, drag handles, block actions, bubble menu
Both editing modelsInstall both packagesUse each package in the parts of the application that need it

How To Use editorcn

1. Register editorcn with shadcn

Add the registry namespace to components.json:

{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "registries": {
    "@editorcn": "https://editorcn.vercel.app/r/{name}.json"
  }
}

2. Install the Toolbar Editor

The registry version writes the editor source under components/editor/.

npx shadcn@latest add @editorcn/editor

Use npm when the editor should stay as a versioned package:

npm install @editorcn/editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-link @tiptap/extension-placeholder

The example below uses Underline, TextAlign, and CharacterCount as well:

npm install @tiptap/extension-underline @tiptap/extension-text-align @tiptap/extension-character-count

3. Import the Styles

Load the shadcn globals before the local editor stylesheet:

import "@/app/globals.css";
import "@/components/editor/style.css";

For an npm installation:

import "@editorcn/editor/style.css";

Basic Toolbar Editor Usage

Use the editor in a client component and set immediatelyRender: false for the Next.js client-side editor instance:

"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 { CharacterCount } from "@tiptap/extension-character-count";
import { Link, RichTextEditor } from "@/components/editor";
import "@/components/editor/style.css";
export function PostEditor() {
  const editor = useEditor({
    immediatelyRender: false,
    shouldRerenderOnTransaction: false,
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3] },
      }),
      Link,
      Underline,
      TextAlign.configure({
        types: ["heading", "paragraph"],
      }),
      Placeholder.configure({
        placeholder: "Write your article...",
      }),
      CharacterCount,
    ],
    content: "",
  });
  return (
    <RichTextEditor editor={editor}>
      <RichTextEditor.Toolbar sticky>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Bold />
          <RichTextEditor.Italic />
          <RichTextEditor.Underline />
          <RichTextEditor.Strikethrough />
        </RichTextEditor.ControlsGroup>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.H1 />
          <RichTextEditor.H2 />
          <RichTextEditor.H3 />
        </RichTextEditor.ControlsGroup>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.BulletList />
          <RichTextEditor.OrderedList />
          <RichTextEditor.Blockquote />
        </RichTextEditor.ControlsGroup>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.AlignLeft />
          <RichTextEditor.AlignCenter />
          <RichTextEditor.AlignRight />
        </RichTextEditor.ControlsGroup>
        <RichTextEditor.ControlsGroup>
          <RichTextEditor.Link />
          <RichTextEditor.Unlink />
          <RichTextEditor.Undo />
          <RichTextEditor.Redo />
        </RichTextEditor.ControlsGroup>
      </RichTextEditor.Toolbar>
      <RichTextEditor.Content />
      <RichTextEditor.Footer showWordCount />
    </RichTextEditor>
  );
}

Toolbar Editor API

ComponentPublic Props
RichTextEditoreditor, children, className, variant, labels, icons
RichTextEditor.Toolbarsticky, stickyOffset, className
RichTextEditor.ControlsGroupchildren, className
RichTextEditor.ContentclassName
RichTextEditor.FootershowWordCount, sticky, stickyOffset, wordCountClassName, wordCountFormatter, className, children
RichTextEditor.BubbleMenueditor
RichTextEditor.Controlactive, interactive, standard button attributes

variant accepts default, subtle, or compact. The default value is default.

Toolbar Controls and Extensions

The editorcn Link extension extends Tiptap’s link extension and powers the link popover and Mod-K shortcut.

YouTubeEmbed accepts standard YouTube watch URLs, Shorts URLs, live URLs, shortlinks, or video IDs. TwitterEmbed accepts twitter.com and x.com post URLs or numeric post IDs.

ExtensionControls
@tiptap/starter-kitBold, Italic, Strikethrough, ClearFormatting, Code, CodeBlock, H1 through H6, BulletList, OrderedList, Blockquote, Hr, Undo, Redo
@tiptap/extension-underlineUnderline
@tiptap/extension-text-alignAlignLeft, AlignCenter, AlignRight, AlignJustify
@tiptap/extension-highlightHighlight
@tiptap/extension-subscriptSubscript
@tiptap/extension-superscriptSuperscript
editorcn Link extensionLink, Unlink
editorcn embed extensionsYouTubeEmbed, TwitterEmbed

Syntax Highlighting

@editorcn/editor ships with CodeBlockLowlight and lowlight configuration for 21 languages. The code block UI can show language-specific icons through DEFAULT_LANGUAGE_ICONS.

Register another highlight.js language when it is not part of that built-in set:

import { common, createLowlight } from "lowlight";
import python from "highlight.js/lib/languages/python";
const lowlight = createLowlight(common);
lowlight.register({
  python,
});

Save Editor Changes

Read HTML inside Tiptap’s onUpdate callback when editor content belongs in form or application state:

interface ArticleEditorProps {
  value: string;
  onChange: (html: string) => void;
}
export function ArticleEditor({
  value,
  onChange,
}: ArticleEditorProps) {
  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>
  );
}

Install the Block Editor

The registry version writes its source under components/block-editor/.

npx shadcn@latest add @editorcn/block-editor

Install the base npm package and required editor dependencies with:

npm install @editorcn/block-editor @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/suggestion @tiptap/extension-drag-handle-react lucide-react

Add extensions for the nodes and commands used by your implementation:

npm install @tiptap/extension-placeholder @tiptap/extension-underline @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-image @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-header

Basic Block Editor Usage

"use client";
import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import {
  BlockEditor,
  SlashCommand,
  getSlashCommandSuggestion,
} from "@/components/block-editor";
import "@/components/block-editor/style.css";
export function WorkspaceEditor() {
  const editor = useEditor({
    immediatelyRender: false,
    shouldRerenderOnTransaction: false,
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3] },
      }),
      Placeholder.configure({
        placeholder: "Type / for commands...",
      }),
      SlashCommand.configure({
        suggestion: getSlashCommandSuggestion(),
      }),
    ],
  });
  return <BlockEditor editor={editor} />;
}

Block Editor API

APIPurpose
BlockEditorRoot editor UI
editorTiptap Editor instance
childrenCustom child layout
classNameRoot CSS classes
labelsPartial BlockEditorLabels overrides
iconsPartial BlockEditorIcons overrides
BlockEditor.ContentContent area with drag handle and block actions
BlockEditor.BubbleMenuFloating inline-formatting menu
SlashCommandTiptap extension for / commands
defaultSlashCommandItemsDefault command definitions
getSlashCommandSuggestion()Creates the Tiptap suggestion configuration
DEFAULT_ICONSDefault Block Editor icons
DEFAULT_LANGUAGE_ICONSCode language icons

Default Slash Commands

Task List content requires the Tiptap TaskList and TaskItem extensions. Syntax highlighting for Code requires @tiptap/extension-code-block-lowlight and lowlight.

Image and Table commands require their Tiptap extensions plus custom slash command items.

CommandNode or Action
TextParagraph
Heading 1H1
Heading 2H2
Heading 3H3
Bullet ListUnordered list
Numbered ListOrdered list
Task ListChecklist
QuoteBlockquote
CodeCode block
DividerHorizontal rule

Custom Slash Commands

A SlashCommandSuggestionItem can define id, title, description, keywords, icon, and command.

import Image from "@tiptap/extension-image";
import {
  SlashCommand,
  defaultSlashCommandItems,
  getSlashCommandSuggestion,
} from "@/components/block-editor";
import type {
  SlashCommandSuggestionItem,
} from "@/components/block-editor";
const commandItems: SlashCommandSuggestionItem[] = [
  ...defaultSlashCommandItems,
  {
    id: "image",
    title: "Image",
    description: "Insert an image by URL.",
    keywords: ["image", "photo"],
    command: ({ editor, range }) => {
      const url = window.prompt("Image URL");
      if (!url) return;
      editor
        .chain()
        .focus()
        .deleteRange(range)
        .setImage({ src: url })
        .run();
    },
  },
];
const editor = useEditor({
  extensions: [
    StarterKit,
    Image,
    SlashCommand.configure({
      suggestion: getSlashCommandSuggestion(commandItems),
    }),
  ],
});

Bubble Menu and Block Actions

The Block Editor bubble menu contains:

  • Node type selector.
  • Bold, italic, underline, strikethrough, and code.
  • Link insertion and removal.
  • Left, center, and right alignment.

The drag handle opens Copy and Delete actions for the active block. Drag the handle to change block order.

Save HTML, JSON, or Plain Text

Both editor packages use Tiptap serialization. Choose the output format before storing the document:

const html = editor.getHTML();
const json = editor.getJSON();
const text = editor.getText();

HTML preserves configured formatting and block nodes. JSON returns the ProseMirror document tree. Plain text removes formatting.

The Block Editor uses the identical serialization format. YouTube and Twitter embed extensions from @editorcn/editor can also be registered with the Block Editor.

Render Saved Content with StaticRenderer

Install the local source through the editorcn registry:

npx shadcn@latest add @editorcn/static-renderer

For npm:

npm install @editorcn/static-renderer

Import the component and its stylesheet:

import { StaticRenderer } from "@/components/static-renderer";
import "@/components/static-renderer/style.css";
interface ArticleProps {
  content: string;
}
export function Article({ content }: ArticleProps) {
  return <StaticRenderer content={content} />;
}

StaticRenderer Props

StaticRenderer uses dangerouslySetInnerHTML. Sanitize editor HTML when content can come from an untrusted author.

YouTube and Twitter nodes render as placeholder cards in the static stylesheet. Replace their serialized data-type, data-src, and data-tweet-id nodes with the application iframe or widget when the published page needs live embeds.

PropTypeDefault
contentstringRequired
asElementType"div"
classNamestringNone
Standard HTML attributesHTMLAttributesNone

Styling and Theming

editorcn reads the shadcn/ui CSS variables already defined by the application.

VariableMain Effect
--backgroundEditor and toolbar background
--foregroundText
--primaryLinks, active controls, checkboxes, syntax accents
--borderEditor border, toolbar dividers, blockquotes, code blocks
--mutedCode and secondary backgrounds
--muted-foregroundPlaceholder and secondary text
--destructiveDestructive actions and syntax errors
--accent-foregroundSyntax accents
--radiusEditor, code block, and checkbox radius
--font-monoCode
--font-sansEditor text

Scope another token set around one editor:

.article-editor-theme {
  --primary: oklch(0.7 0.25 150);
  --radius: 0.25rem;
}
<div className="article-editor-theme">
  <RichTextEditor editor={editor}>
    <RichTextEditor.Toolbar>
      <RichTextEditor.Bold />
    </RichTextEditor.Toolbar>
    <RichTextEditor.Content />
  </RichTextEditor>
</div>

Toolbar Variants

VariantAppearance
defaultBordered editor with segmented toolbar controls
subtleBorderless card treatment with a soft shadow
compactReduced spacing and smaller toolbar icons
<RichTextEditor editor={editor} variant="compact">
  <RichTextEditor.Toolbar>
    <RichTextEditor.Bold />
    <RichTextEditor.Italic />
  </RichTextEditor.Toolbar>
  <RichTextEditor.Content />
</RichTextEditor>

Labels, Icons, and Custom Controls

Pass partial labels and icons objects to RichTextEditor. editorcn exports DEFAULT_LABELS, DEFAULT_ICONS, and DEFAULT_LANGUAGE_ICONS.

<RichTextEditor
  editor={editor}
  labels={{
    boldControlLabel: "Bold text",
    linkControlLabel: "Insert link",
  }}
  icons={{
    boldControlIcon: <CustomBoldIcon />,
  }}
>
  {/* editor UI */}
</RichTextEditor>

Build a custom toolbar action with RichTextEditor.Control and useRichTextEditorContext.

BlockEditor accepts its own labels and icons overrides for slash commands, the bubble menu, drag handle, block actions, and code language selector.

Registry Source Ownership and Updates

Registry installation copies editorcn source into components/editor/, components/block-editor/, or components/static-renderer/. Tiptap peer dependencies stay in node_modules.

InstallationSource LocationLocal EditingUpdates
shadcn registryProject components/ directoryFull source editingRe-run shadcn add
npmnode_modules/@editorcn/*Props and CSS customizationPackage manager update

The shadcn CLI prompts before overwriting editor files. Review local modifications before accepting replacements.

npx shadcn@latest add @editorcn/editor

Alternatives and Related Resources

shadcn-labs

shadcn-labs

Pushing the limits of shadcn/ui ecosystem.

Leave a Reply

Your email address will not be published. Required fields are marked *