@rozie-ui/tiptap-react
v0.5.1
Published
Idiomatic React rich-text editor — one Rozie source compiled to React wrapping TipTap.
Maintainers
Readme
@rozie-ui/tiptap-react
Idiomatic react TipTap — a cross-framework rich-text editor component compiled from one Rozie source wrapping TipTap (the ProseMirror-based headless editor). Two-way html content binding, a batteries-included toolbar (or bring your own via the toolbar slot), a 25-verb imperative command handle, and editorProps/extensions passthroughs. This package is generated; do not edit src/ by hand.
Install
npm i @rozie-ui/tiptap-reactPeer dependencies: @tiptap/core, @tiptap/starter-kit, @tiptap/extensions and @tiptap/extension-bubble-menu (all ^3) + react + react-dom. Install them alongside this package. Optional, loaded only when used: @tiptap/extension-character-count (maxLength / #count), @tiptap/extension-image (uploadImage) and @tiptap/extension-floating-menu (the floatingMenu slot).
Also installed: @rozie/runtime-react — Rozie's small, tree-shaken runtime helper package (controllable state, keyboard navigation, event modifiers, and safe interpolation). It arrives as a regular dependency, so npm pulls it for you. Your bundler keeps only the helpers this component actually uses — typically a few hundred bytes to a few KB, minified and gzipped. What's in it and what it costs.
Usage
import { useState } from 'react';
import { TipTap } from '@rozie-ui/tiptap-react';
export function Demo() {
const [html, setHtml] = useState('<p>Hello <strong>world</strong></p>');
return <TipTap html={html} onHtmlChange={setHtml} placeholder="Start writing…" />;
}Props
| Name | Type | Default | Two-way (model) |
| --- | --- | --- | :---: |
| html | String | "<p>Start writing…</p>" | ✓ |
| editable | Boolean | true | |
| placeholder | String | "" | |
| autofocus | Boolean | false | |
| editorClass | String | "" | |
| ariaLabel | String | "Rich text editor" | |
| editorProps | Object | {} | |
| extensions | Array | [] | |
| starterKit | Object | {} | |
| nodeSpecs | Array | [] | |
| uploadImage | Function | null | |
| maxLength | Number | null | |
| enforceMaxLength | Boolean | false | |
| bubbleMenuShouldShow | Function | null | |
Events
| Event | Description |
| --- | --- |
| update | The document changed — payload is the new HTML string. |
| selectionUpdate | The selection (caret/range) moved. |
| focus | The editor gained focus. |
| blur | The editor lost focus. |
| ready | The editor exists — the live TipTap Editor instance. Fires once per mount. Handle verbs (focusEditor(), setContent(), …) work from here on; with an optional extension in use (maxLength, uploadImage, the floatingMenu slot) construction waits for its import, so this is the moment to act rather than a fixed delay. |
| error | An optional extension (floatingMenu, image, count) failed to load via dynamic import() — payload is { extension, error }. The editor still constructs WITHOUT that extension (degrade); a network blip never leaves a mounted <TipTap> permanently blank. |
Imperative handle
Beyond props, the component exposes imperative methods (declared once in the Rozie source via $expose). Grab a handle with the native ref mechanism and call them directly:
import { useRef } from 'react';
import { TipTap, type TipTapHandle } from '@rozie-ui/tiptap-react';
const editor = useRef<TipTapHandle>(null);
// <TipTap ref={editor} ... />
editor.current?.toggleBold();
const html = editor.current?.getHTML();| Method | Description |
| --- | --- |
| getEditor | Return the underlying TipTap Editor instance for direct API access (commands, state, schema, extension storage). |
| focusEditor | Focus the editor — place the caret in the document. |
| blurEditor | Blur the editor — remove focus from the document. |
| getHTML | Return the current document serialized as an HTML string. |
| getJSON | Return the current document as a ProseMirror JSON object (JSONContent). |
| getText | Return the current document as a plain-text string (word/char counts, search indexing, plaintext export). |
| setContent | Replace the document content — setContent(html). Echo-guarded: reflects into the bound html model without bouncing an extra update. |
| clearContent | Clear the document to an empty paragraph (reflects the empty value into the bound html model). |
| toggleBold | Toggle bold on the current selection. |
| toggleItalic | Toggle italic on the current selection. |
| toggleHeading | Toggle a heading at the given level — toggleHeading(level) (defaults to 1). |
| toggleBulletList | Toggle a bullet list at the current selection. |
| toggleUnderline | Toggle underline on the current selection. |
| toggleOrderedList | Toggle an ordered (numbered) list at the current selection. |
| undo | Undo the last change. |
| redo | Redo the last undone change. |
| chain | Return a focused TipTap command chain for composing commands — e.g. chain().toggleBold().toggleItalic().run() (null before mount). |
| isActive | Whether a mark/node is active in the current selection — isActive(name, attrs?) (e.g. isActive("heading", { level: 2 })). Drives custom-toolbar active styling. False before mount. |
| can | Return the command-availability chain — can().chain().focus().toggleBold().run() returns a boolean — for enabling/disabling custom-toolbar buttons. null before mount. |
| isEmpty | Whether the document is empty — drives empty-state UI and submit-gating. true before mount. |
| getCharacterCount | Return the current character count. Reads the CharacterCount extension's live storage when registered (maxLength set or the #count slot filled), else falls back to getText().length. Always a number — 0 before mount. |
| getWordCount | Return the current word count. Reads the CharacterCount extension's live storage when registered, else falls back to a whitespace-split count of getText(). Always a number — 0 before mount. |
| openLinkEditor | Open the link editor on the current selection (create mode) — the imperative equivalent of clicking the toolbar Link button. Surfaces the editor prefilled with any existing link href; no-op before mount. |
| setLink | Apply or replace a link on the current selection, widening to the whole link mark first — setLink({ href }), with any additional stock attrs (target, rel, class, title) forwarded verbatim. An attrs object without a non-empty href is ignored. Attrs the registered Link extension does not declare are dropped by the extension itself — persisting a custom attribute requires Link.extend({ addAttributes }) via the extensions prop. No-op before mount. |
| unsetLink | Remove the link mark from the current selection, widening to the whole link first. No-op before mount. |
Slots
When you fill the toolbar slot the internal toolbar is replaced by your own UI, which receives the live editor so its buttons can drive editor.chain().focus()…run():
renderToolbar={({ editor }) => <MyToolbar editor={editor} />}| Slot | Params | | --- | --- | | count | characters, words, maxLength, over | | toolbar | editor | | bubbleMenu | editor | | floatingMenu | editor | | linkEditor | editor, href, attrs, setLink, unsetLink, close | | nodeView | node, selected, updateAttributes, getPos, editor, contentDOM |
