composed · Composed pattern

RichTextEditor

@devalok/shilp-sutra/composed/rich-text-editorView in Storybook
Live preview coming

Hand-curated previews ship in rolling waves. See it live in Storybook →

Reference

Exports: RichTextEditor, RichTextViewer

Props

RichTextEditor

content: string (HTML string)
placeholder: string (default: "Start writing...")
onChange: (html: string) => void
className: string
editable: boolean (default: true)
onImageUpload?: (file: File) => Promise<string> — return URL. If omitted, images paste as base64
onFileUpload?: (file: File) => Promise<{ url: string; name: string; size: number }> — enables file attachments
mentions?: MentionItem[] — static list for @mention autocomplete
onMentionSearch?: (query: string) => Promise<MentionItem[]> — async search, takes precedence over static mentions
toolbar?: ToolbarItem[] — whitelist of toolbar items to show. Omit to show all.
onMentionSelect?: (item: MentionItem) => void — called when a mention is selected

ToolbarItem: 'bold' | 'italic' | 'underline' | 'strike' | 'highlight' | 'h2' | 'h3' | 'blockquote' | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock' | 'link' | 'image' | 'file' | 'hr' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'emoji' | 'undo' | 'redo'

MentionItem: { id: string; label: string; avatar?: string }

RichTextViewer

content: string (REQUIRED, HTML string)
className: string

Defaults

RichTextEditor: placeholder="Start writing...", editable=true

Example

<RichTextEditor content={html} onChange={setHtml} placeholder="Write your message..." />

<RichTextEditor
  content={html}
  onChange={setHtml}
  mentions={[{ id: '1', label: 'Aarav' }]}
  onImageUpload={async (file) => uploadAndReturnUrl(file)}
  onFileUpload={async (file) => ({ url: uploadUrl, name: file.name, size: file.size })}
/>

<RichTextViewer content={savedHtml} />

Composability

  • Two exports — editor + viewer. RichTextEditor for composition; RichTextViewer for read-only rendering of saved HTML. Both share the same prose styling so round-trip display matches the editor.
  • TipTap v3 bundled — no @tiptap/* install needed. Consumers can't mix in arbitrary TipTap extensions without forking.
  • Toolbar whitelist via toolbar prop: Pass an array of ToolbarItem names to show only those buttons. Omit to show all. Dividers auto-collapse between empty groups.
  • Image upload: Without onImageUpload, pasted/dropped images are inlined as base64 (HTML bloats fast). Provide the handler to upload and return a URL.
  • Mentions: Static mentions array OR async onMentionSearch (which takes precedence). The viewer always renders mentions correctly from saved HTML — no mention props needed on the viewer side.
  • For chat composition specifically (AI + team chat with streaming / slash commands), use RichChatInput — it's built on the same foundation but pre-configured for chat UX.
  • Pairs with MarkdownViewer — many teams use RichTextEditor for compose (WYSIWYG), but render saved content as markdown for simpler serialization. Convert HTML ↔ markdown at the storage boundary.

Gotchas

  • Tiptap is bundled — no need to install @tiptap/* packages separately
  • Emoji picker requires @emoji-mart/react + @emoji-mart/data peers
  • Images without onImageUpload are stored as base64 in HTML — large images bloat content
  • Mention rendering in viewer always works (no mention props needed, just the HTML)
  • Features: bold, italic, underline, strikethrough, highlight, headings, blockquote, lists, task lists, code, links, images, file attachments, mentions, emoji, text alignment, horizontal rule

Changes

v0.33.0

  • Added emojiSet prop — EmojiSet type for consistent emoji art style rendering
  • Changed TipTap v2 → v3 upgrade (useEditorState, immediatelyRender: false for SSR, ListKit)
  • Added EmojiNode + createEmojiSuggestion(set) registered internally

v0.30.0

  • Added toolbar prop — whitelist of ToolbarItem names to control which toolbar buttons appear. Dividers render only between groups that have visible items. ToolbarItem type exported from barrel.

v0.18.0

  • Fixed Use ref to track internal changes, prevent update loop

v0.9.0

  • Changed All @tiptap/* packages moved from peerDependencies to bundled build-time dependencies — consumers no longer need to install tiptap separately

v0.8.0

  • Fixed Emoji picker now renders above the editor (not clipped by overflow)
  • Fixed Link/image URL injection prevented via protocol validation
  • Fixed Escape key in emoji picker no longer closes parent dialogs
  • Fixed Tiptap peer deps tightened to >=2.27.2 <3.0.0

v0.7.0

  • Added Initial release — full-featured tiptap-based rich text editing with toolbar, mentions, emoji, image, alignment

v0.1.1

  • Fixed Added content sync effect so editor updates when content prop changes externally