MUITipTapRte
MUITipTapRte wraps Tiptap's headless useEditor hook,
backed by an HTML string value — a lighter, dependency-thinner alternative to
MUIRichTextEditor (CKEditor 5). Since Tiptap
ships no UI of its own, this component includes a hand-built formatting
toolbar (bold, italic, underline, strikethrough, headings, lists, blockquote,
code block, links, alignment, undo/redo) driven entirely by the editor's own
extensions.
Usage
import MUITipTapRte, {
DefaultEditorExtensions,
type MUITipTapRteProps
} from '@nish1896/mui-components/misc/tiptap-rte';The value of bio will be an HTML string.
const [bio, setBio] = useState('');
<MUITipTapRte
fieldName="bio"
value={bio}
onValueChange={({ newValue }) => setBio(newValue)}
placeholder="Tell us about yourself"
/>To customize the toolbar/formatting options, pass a custom editorExtensions
array (or extend DefaultEditorExtensions). Every toolbar button checks whether
its backing extension is actually loaded before rendering, so a custom set missing
e.g. Heading or TextAlign simply omits those buttons instead of throwing when
clicked:
import MUITipTapRte, { DefaultEditorExtensions } from '@nish1896/mui-components/misc/tiptap-rte';
import Table from '@tiptap/extension-table';
<MUITipTapRte
fieldName="bio"
value={bio}
onValueChange={({ newValue }) => setBio(newValue)}
editorExtensions={[...DefaultEditorExtensions, Table]}
/>Reach into useEditor itself for anything not exposed as its own prop
via editorOptions — pass onCreate/onFocus/onUpdate/onBlur here
directly and they reach useEditor unmodified. onUpdate specifically is
never replaced, only chained: useEditor's onUpdate first calls onValueChange
with the editor's latest HTML, then calls your editorOptions.onUpdate, so
value is always in sync by the time your own handler runs.
editorOptions is layered underneath only this component's own required
wiring (content/editable and the accessibility attributes), so it can't
break the controlled-value contract; editorProps.attributes is deep-merged
rather than replaced.
<MUITipTapRte
fieldName="bio"
value={bio}
onValueChange={({ newValue }) => setBio(newValue)}
editorOptions={{
autofocus: true,
onCreate: ({ editor }) => console.log('ready', editor),
onFocus: ({ editor }) => console.log('focused', editor),
onBlur: ({ editor }) => console.log('blurred', editor),
onUpdate: ({ editor }) => console.log('newContent', editor.getHTML()),
editorProps: { attributes: { spellcheck: 'false' } }
}}
/>containerProps/contentContainerProps style the outer bordered box and the scrollable
box directly wrapping the editor content respectively — both merge their sx with this
component's own base styles rather than replacing them. renderToolbar replaces the
built-in toolbar entirely (pass () => null to hide it), receiving the live editor
instance and the field's resolved disabled state so a custom toolbar can drive the same
editor.chain()...run() commands and respect the same disabled state:
<MUITipTapRte
fieldName="bio"
value={bio}
onValueChange={({ newValue }) => setBio(newValue)}
containerProps={{ sx: { borderColor: 'primary.main' } }}
contentContainerProps={{ sx: { minHeight: 240 } }}
renderToolbar={(editor, disabled) => (
<Button
disabled={disabled}
onClick={() => editor.chain().focus().toggleBold().run()}
>
Bold
</Button>
)}
/>API
Pass a custom editorExtensions to control the Tiptap extension set (and, in turn, which
toolbar buttons render). Props marked with
* are required.
| Name | Type | Description |
|---|---|---|
fieldName* | string | Name/path of the field. Used to derive generated ids and the default label. |
value | string | null | Current editor HTML string. |
onValueChange* | ({ newValue, editor }) => void | Called when the editor content changes, with the updated HTML string and editor instance. |
editorOptions | UseEditorOptions | Additional options passed straight through to the useEditor hook — e.g. onCreate, onFocus, onBlur, autofocus, editorProps.handleDOMEvents, parseOptions, injectCSS. editorProps.attributes is deep-merged instead of replaced.onUpdate is also passed through, but this component's own onUpdate (which calls onValueChange) always runs first. |
editorExtensions | AnyExtension[] | Tiptap extensions passed to useEditor. Defaults to this package's DefaultEditorExtensions. The built-in toolbar omits any button whose backing extension isn't loaded, so a custom set missing e.g. Heading or TextAlign simply hides those controls instead of throwing when clicked. |
containerProps | BoxProps | Props forwarded to the outer bordered container wrapping the toolbar and editor content. containerProps.sx is merged with the component's own base styles (border, radius, focus ring) rather than replacing them, and accepts any sx form — object, array, or function. |
contentContainerProps | BoxProps | Props forwarded to the scrollable Box directly wrapping EditorContent (padding, min/max height, and the .ProseMirror/placeholder styling). contentContainerProps.sx is merged the same way as containerProps.sx. |
renderToolbar | (editor: Editor, disabled: boolean) => ReactNode | Custom toolbar renderer, called with the live editor instance and the field's resolved disabled state. Defaults to this package's built-in Toolbar. Pass () => null to hide the toolbar entirely. |
required | boolean | Indicates that the field is mandatory by adding an asterisk symbol (*) to the form label and setting the relevant accessibility attributes. |
placeholder | string | Placeholder text shown when the editor is empty. Always applied via an internal Placeholder extension appended after editorExtensions (or DefaultEditorExtensions), regardless of which set is active. |
disabled | boolean | When true, disables the field and associated controls. |
label | ReactNode | Label displayed for the field. Defaults to a human-readable label derived from fieldName, e.g. firstName becomes "First Name". |
showLabelAboveFormField | boolean | Whether the field label renders above the control. This control has no built-in inline label, so it defaults to true; pass false to hide the visible label (the accessible name is still applied).Default: true |
hideLabel | boolean | When true, hides the rendered field label while preserving accessible labeling where possible. |
formLabelProps | FormLabelProps | FormLabelProps forwarded to the internal FormLabel. The id is managed by the component. Multiple fields can be configured using the ConfigProvider component. |
errorMessage | string | string[] | Validation error for the field — pass a single message string, or a string[] when the field can fail multiple rules at once (every message is shown together). A non-empty string or array puts the field in an error state; undefined/''/[] clear it.Normalize your form library's error shape to this at the call site (e.g. an RHF FieldError via its .message). Use renderError to customize how the message(s) are rendered. |
renderError | (errors: string[]) => ReactNode | Custom renderer for the resolved error message(s), called only when the field is in an error state. Always receives a string[] — use errors[0] for the common single-message case, or map over errors when a field fails several rules. By default a single message renders as text and multiple messages render on separate lines. |
hideErrorMessage | boolean | If true, hides the error message text while keeping the field in an error state. |
helperText | ReactNode | Content displayed in the FormHelperText component below the field when there is no visible validation error. |
formHelperTextProps | FormHelperTextProps | FormHelperTextProps forwarded to the internal FormHelperText. The id is managed by the component. Multiple fields can be configured using the ConfigProvider component. |
customIds | { field, label, helperText, error } | Overrides the default field, label, helper text, and error IDs used for accessibility. |
Playground
Driven by plain React state — the value is an HTML string; a required editor with a custom
label and validation, plus one with the label above the field.