npm.io
1.0.1 • Published 3d ago

@draftpad/rich-editor

Licence
MIT
Version
1.0.1
Deps
0
Size
222 kB
Vulns
0
Weekly
260

@draftpad/rich-editor

A portable, headless-ready rich-text editor for React. Built on contenteditable + the Selection/Range API — no ProseMirror, no Quill, no heavy runtime dependency.

  • Full formatting toolbar (bold, italic, headings, lists, code, alignment, colors, font)
  • Find & Replace with live highlighting
  • Hyperlink insert/remove dialog
  • Ctrl+F / Ctrl+K / Ctrl+P keyboard shortcuts
  • Imperative insertText handle (great for AI text injection)
  • CSS custom properties for zero-friction theming
  • Tree-shakeable: use RichEditor as a complete solution, or import individual pieces

Installation

npm install @draftpad/rich-editor
# peer deps (install if not already present)
npm install react react-dom lucide-react

Quick start

Next.js (App Router)

Import the stylesheet once in your root layout:

// app/layout.tsx
import '@draftpad/rich-editor/styles';

Then use the editor in any client component:

'use client';
import { useState } from 'react';
import { RichEditor } from '@draftpad/rich-editor';

export default function MyEditor() {
  const [html, setHtml] = useState('');

  return (
    <div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
      <RichEditor content={html} onChange={setHtml} />
    </div>
  );
}

The package outputs "use client" at the top of every bundle file, so Next.js automatically treats all exports as client components.

Plain React (Vite / CRA)
// main.tsx
import '@draftpad/rich-editor/styles';
import { useState } from 'react';
import { RichEditor } from '@draftpad/rich-editor';

function App() {
  const [html, setHtml] = useState('');
  return <RichEditor content={html} onChange={setHtml} />;
}

API

<RichEditor>

The all-in-one component: toolbar + find bar + editor canvas + link dialog.

import { RichEditor, RichEditorHandle } from '@draftpad/rich-editor';
Props
Prop Type Default Description
content string Required. HTML string shown in the editor. Update this from outside to push remote changes (e.g. Yjs).
onChange (html: string) => void Required. Called whenever the user edits the content.
readOnly boolean false Renders a read-only view; hides the toolbar.
placeholder string 'Start writing…' Placeholder shown when the editor is empty.
onAIToggle () => void Called when the user clicks the AI (✦) toolbar button. Wire this to open your AI panel.
onTextSelect (text: string) => void Called on every selection change with the currently selected plain text.
Ref handle (RichEditorHandle)

Use ref to imperatively insert text at the current cursor position — useful for injecting AI-generated content.

import { useRef } from 'react';
import { RichEditor, RichEditorHandle } from '@draftpad/rich-editor';

function Page() {
  const editorRef = useRef<RichEditorHandle>(null);

  const handleAIResult = (text: string) => {
    editorRef.current?.insertText(text);
  };

  return (
    <RichEditor
      ref={editorRef}
      content={html}
      onChange={setHtml}
      onAIToggle={() => setAIPanelOpen(true)}
    />
  );
}
Method Signature Description
insertText (text: string) => void Inserts plain text at the current cursor position and triggers onChange.

Keyboard shortcuts
Shortcut Action
Ctrl / Cmd + B Bold
Ctrl / Cmd + I Italic
Ctrl / Cmd + U Underline
Ctrl / Cmd + K Insert / edit hyperlink
Ctrl / Cmd + F Open Find & Replace bar
Ctrl / Cmd + P Print
Ctrl / Cmd + Z Undo
Ctrl / Cmd + Y Redo

Theming

Import the stylesheet once and override any --re-* variable in :root:

/* your-app.css */
@import '@draftpad/rich-editor/styles';   /* or import in JS/TS */

:root {
  /* Colors */
  --re-text-color:        #111827;   /* body text */
  --re-heading-color:     #111827;   /* h1 / h2 / h3 */
  --re-placeholder-color: #9ca3af;   /* placeholder hint */
  --re-link-color:        #6366f1;   /* anchor tags */
  --re-accent-color:      #6366f1;   /* blockquote border, focus rings */
  --re-blockquote-color:  #6b7280;   /* blockquote text */
  --re-code-bg:           #f3f4f6;   /* inline code & pre background */
  --re-code-color:        #4f46e5;   /* inline code text */

  /* Typography */
  --re-font-family:  Inter, system-ui, sans-serif;
  --re-mono-font:    'JetBrains Mono', 'Fira Code', monospace;
  --re-font-size:    16px;
  --re-line-height:  1.8;
}
Dark theme example
:root {
  --re-text-color:        #F4F4F5;
  --re-heading-color:     #F4F4F5;
  --re-placeholder-color: #52525B;
  --re-link-color:        #818CF8;
  --re-accent-color:      #6366F1;
  --re-blockquote-color:  #A1A1AA;
  --re-code-bg:           #1F1F23;
  --re-code-color:        #A5B4FC;
}

The defaults ship as light-theme values, so you get a working editor with zero config on any white-background app.


Supported format commands

The toolbar exposes these commands via onFormat(format, value?). If you build a custom toolbar using useEditorFormat, these are the strings to pass:

Command Value Description
bold Toggle bold
italic Toggle italic
underline Toggle underline
strikethrough Toggle strikethrough
superscript Toggle superscript
subscript Toggle subscript
clearFormat Remove all inline formatting
undo Undo
redo Redo
h1 Format block as <h1>
h2 Format block as <h2>
h3 Format block as <h3>
bullet Unordered list
ordered Ordered list
quote Blockquote
code Wrap selection in <code>
alignLeft Left-align
alignCenter Center
alignRight Right-align
alignJustify Justify
foreColor '#rrggbb' Text color (uses Range API — reliable across browsers)
hiliteColor '#rrggbb' or 'transparent' Highlight / background color
fontName e.g. 'Georgia, serif' Change font family
fontSize e.g. '18px' Change font size
link Open link dialog (requires onOpenLink wired up)

Using individual components

All sub-components are exported separately for custom layouts.

<EditorToolbar>
import { EditorToolbar } from '@draftpad/rich-editor';

<EditorToolbar
  onFormat={(format, value) => { /* call handleFormat */ }}
  onAI={() => { /* open AI panel */ }}
  onFindOpen={() => { /* open find bar */ }}
  formatState={formatState}   // from useEditorFormat
  isViewer={false}            // hides toolbar when true
/>
<FindReplaceBar>
import { FindReplaceBar } from '@draftpad/rich-editor';

<FindReplaceBar
  editorRef={editorRef}           // RefObject<HTMLDivElement | null>
  isHighlighting={isHighlighting} // { current: boolean } — mutable ref
  onClose={() => setShowFind(false)}
/>
<LinkDialog>
import { LinkDialog } from '@draftpad/rich-editor';

<LinkDialog
  initialUrl="https://example.com"   // pre-fills if editing an existing link
  onConfirm={(url) => applyLink(url)}
  onRemove={() => removeLink()}
  onClose={() => setShowLink(false)}
/>

useEditorFormat hook

Use this when you want full control over the editor DOM and toolbar layout but don't want to rewrite selection management and execCommand wiring.

import { useRef } from 'react';
import { useEditorFormat, EditorToolbar } from '@draftpad/rich-editor';

function MyCustomEditor() {
  const editorRef = useRef<HTMLDivElement>(null);
  const [showLink, setShowLink] = useState(false);
  const [linkInitial, setLinkInitial] = useState('');

  const { formatState, handleFormat, handleEditorBlur, applyLink, removeLink } =
    useEditorFormat({
      editorRef,
      onContentChange: () => {
        onChange(editorRef.current?.innerHTML ?? '');
      },
      onOpenLink: (initialUrl) => {
        setLinkInitial(initialUrl);
        setShowLink(true);
      },
    });

  return (
    <>
      <EditorToolbar
        onFormat={handleFormat}
        onAI={() => {}}
        onFindOpen={() => {}}
        formatState={formatState}
      />
      <div
        ref={editorRef}
        contentEditable
        suppressContentEditableWarning
        onBlur={handleEditorBlur}
        className="editor-canvas"
      />
      {showLink && (
        <LinkDialog
          initialUrl={linkInitial}
          onConfirm={(url) => { applyLink(url); setShowLink(false); }}
          onRemove={() => { removeLink(); setShowLink(false); }}
          onClose={() => setShowLink(false)}
        />
      )}
    </>
  );
}
useEditorFormat options
Option Type Description
editorRef RefObject<HTMLDivElement | null> Ref attached to your contenteditable div.
onContentChange () => void Called after every formatting operation that mutates the DOM.
onOpenLink (initialUrl: string) => void Called when the user triggers the link command. Open your link dialog here.
useEditorFormat return value
Value Type Description
formatState FormatState Current selection's font, size, text color, highlight color.
handleFormat (format: string, value?: string) => void Applies a format command.
handleEditorBlur () => void Saves selection on blur. Attach to onBlur of your contenteditable.
applyLink (url: string) => void Creates a hyperlink from the saved selection.
removeLink () => void Removes the hyperlink at the saved selection.
FormatState
interface FormatState {
  fontFamily: string;   // current font family at cursor
  fontSize:   string;   // current font size at cursor
  textColor:  string;   // current text color at cursor
  hlColor:    string;   // current highlight color at cursor
}

TypeScript

All types are bundled. No @types/ package needed.

import type {
  RichEditorProps,
  RichEditorHandle,
  FormatState,
} from '@draftpad/rich-editor';

Requirements

Peer dependency Version
react >= 18
react-dom >= 18
lucide-react >= 0.300.0

License

MIT — DraftPad

Keywords