RichTextEditor
freeRichTextEditor combines Tiptap editing with composable formatting tools for notes, comments, and long-form fields.
import { useState } from 'react';
import RichTextEditor from '@/components/composites/RichTextEditor';
const initialContent =
'<h4>Quarterly planning</h4><p>Capture the decisions and next steps here.</p>';
export default function UsageDemo() {
const [htmlLength, setHtmlLength] = useState(0);
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<RichTextEditor
content={initialContent}
onChange={({ html }) => setHtmlLength(html.length)}
/>
<div className="flex justify-end">
<span className="font-mono tabular-nums">{htmlLength} characters</span>
</div>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add RichTextEditorExamples
Custom Toolbar
import RichTextEditor from '@/components/composites/RichTextEditor';
export default function CustomToolbarDemo() {
return (
<div className="w-full max-w-2xl">
<RichTextEditor
content="<p>Use a smaller toolbar for short comments and notes.</p>"
customToolBar={(editor) => (
<>
<RichTextEditor.ToolButtonBold editor={editor} />
<RichTextEditor.ToolButtonItalic editor={editor} />
<RichTextEditor.ToolButtonStrike editor={editor} />
<RichTextEditor.ToolButtonCode editor={editor} />
<RichTextEditor.ToolButtonBlockquote editor={editor} />
<RichTextEditor.ToolButtonHeading
editor={editor}
headingLevel={[2, 3]}
/>
<RichTextEditor.ToolButtonBulletList editor={editor} />
<RichTextEditor.ToolButtonOrderedList editor={editor} />
<RichTextEditor.ToolButtonCodeBlock editor={editor} />
<RichTextEditor.ToolButtonHorizontalRule editor={editor} />
<RichTextEditor.ToolButtonUndo editor={editor} />
<RichTextEditor.ToolButtonRedo editor={editor} />
</>
)}
/>
</div>
);
}Custom Editor
import { useState } from 'react';
import Image from '@tiptap/extension-image';
import { useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import RichTextEditor from '@/components/composites/RichTextEditor';
const initialContent =
'<p>This editor accepts images from the local file picker.</p>';
export default function CustomEditorDemo() {
const [uploadedFile, setUploadedFile] = useState('No image selected');
const editor = useEditor({
extensions: [StarterKit, Image],
content: initialContent,
immediatelyRender: false,
editorProps: {
attributes: {
class: 'p-2 min-h-20 focus:outline-hidden',
},
},
});
return (
<div className="flex w-full max-w-2xl flex-col gap-2">
<RichTextEditor
content={initialContent}
customEditor={editor}
customToolBar={(editor) => (
<>
<RichTextEditor.ToolButtonBold editor={editor} />
<RichTextEditor.ToolButtonItalic editor={editor} />
<RichTextEditor.ToolButtonImage
editor={editor}
onUpload={(file) => setUploadedFile(file.name)}
/>
</>
)}
/>
</div>
);
}Invalid
import { useState } from 'react';
import Button from '@/components/ui/Button';
import RichTextEditor from '@/components/composites/RichTextEditor';
export default function InvalidDemo() {
const [invalid, setInvalid] = useState(true);
return (
<div className="flex w-full max-w-2xl flex-col gap-4">
<RichTextEditor
invalid={invalid}
content="<p>A comment is required before continuing.</p>"
editorContentClass="min-h-[120px]"
/>
<Button
variant="subtle"
onClick={() => setInvalid((current) => !current)}
>
{invalid ? 'Clear invalid state' : 'Mark as invalid'}
</Button>
</div>
);
}With Form
import { useState } from 'react';
import Alert from '@/components/ui/Alert';
import Button from '@/components/ui/Button';
import Form from '@/components/ui/Form';
import RichTextEditor from '@/components/composites/RichTextEditor';
export default function WithFormDemo() {
const [hasContent, setHasContent] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const invalid = submitted && !hasContent;
return (
<div className="flex flex-col gap-4 w-full">
{showSuccess ? (
<Alert
closable
showIcon
variant="success"
role="status"
onClose={() => setShowSuccess(false)}
>
Update saved.
</Alert>
) : null}
<Form
className="w-full max-w-2xl"
onSubmit={(event) => {
event.preventDefault();
setSubmitted(true);
setShowSuccess(hasContent);
}}
>
<Form.Field
asterisk
label="Weekly update"
hint="Share progress, blockers, and next steps."
invalid={invalid}
errorMessage="Write an update before saving."
>
<RichTextEditor
content=""
invalid={invalid}
editorContentClass="min-h-[120px]"
onChange={({ text }) => {
setHasContent(Boolean(text.trim()));
setSubmitted(false);
setShowSuccess(false);
}}
/>
</Form.Field>
<Button type="submit" variant="solid">
Save update
</Button>
</Form>
</div>
);
}API
RichTextEditor is coupled to Tiptap by design. Without customEditor, it creates a StarterKit editor and supplies the default toolbar. With customEditor, the caller owns the editor extensions and its update lifecycle.
| Prop | Description | Type | Default |
|---|---|---|---|
content | Initial HTML, and the HTML value applied when the prop changes. | string | '' |
invalid | Applies the destructive editor state. | boolean | false |
customToolBar | Replaces the default toolbar with a renderer receiving the Tiptap editor and the built-in toolbar component map. | RichTextEditorToolbarRenderer | Default toolbar |
onChange | Called by the internally created editor after an update. | (payload: RichTextEditorChangePayload) => void | — |
editorContentClass | Additional classes applied to the EditorContent element. | string | — |
customEditor | Uses a caller-owned Tiptap editor instance instead of creating one internally. | Editor | null | — |
ref | Ref forwarded to the EditorContent root element. | Ref<HTMLDivElement> | — |
...rest | Remaining EditorContent props, excluding editor, ref, and onChange. | EditorContentProps | — |
Compound Toolbar Exports
The default export also exposes the reusable toolbar pieces and update hook:
RichTextEditor.ToolButton, RichTextEditor.ToolButtonBold, RichTextEditor.ToolButtonItalic, RichTextEditor.ToolButtonStrike, RichTextEditor.ToolButtonCode, RichTextEditor.ToolButtonBlockquote, RichTextEditor.ToolButtonHeading, RichTextEditor.ToolButtonParagraph, RichTextEditor.ToolButtonBulletList, RichTextEditor.ToolButtonOrderedList, RichTextEditor.ToolButtonCodeBlock, RichTextEditor.ToolButtonHardBreak, RichTextEditor.ToolButtonHorizontalRule, RichTextEditor.ToolButtonImage, RichTextEditor.ToolButtonUndo, RichTextEditor.ToolButtonRedo, and RichTextEditor.useEditorUpdate.
Types
import type { ComponentType, ReactNode } from 'react';
import type { Editor, EditorContentProps, JSONContent } from '@tiptap/react';
type RichTextEditorChangePayload = {
text: string;
html: string;
json: JSONContent;
};
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
type ToolbarComponents = {
ToolButtonBold: ComponentType<{ editor: Editor }>;
ToolButtonItalic: ComponentType<{ editor: Editor }>;
ToolButtonStrike: ComponentType<{ editor: Editor }>;
ToolButtonCode: ComponentType<{ editor: Editor }>;
ToolButtonBlockquote: ComponentType<{ editor: Editor }>;
ToolButtonHeading: ComponentType<{
editor: Editor;
headingLevel?: HeadingLevel[];
}>;
ToolButtonBulletList: ComponentType<{ editor: Editor }>;
ToolButtonOrderedList: ComponentType<{ editor: Editor }>;
ToolButtonCodeBlock: ComponentType<{ editor: Editor }>;
ToolButtonHorizontalRule: ComponentType<{ editor: Editor }>;
ToolButtonParagraph: ComponentType<{ editor: Editor }>;
ToolButtonUndo: ComponentType<{ editor: Editor }>;
ToolButtonRedo: ComponentType<{ editor: Editor }>;
};
type RichTextEditorToolbarRenderer = (
editor: Editor,
components: ToolbarComponents,
) => ReactNode;