RichTextEditor
A rich text editor component built on TipTap for editing HTML content with formatting capabilities.
Features
- Rich Text Editing: WYSIWYG editor powered by TipTap
- Comprehensive Formatting: Bold, italic, underline, strikethrough, subscript, superscript
- Text Alignment: Left, center, right, and justify
- Lists: Ordered and unordered lists with indent/outdent
- Links & Media: Hyperlinks, images with resize, and tables
- Toolbar Overflow: Automatic overflow menu for narrow containers
- Customizable: Custom toolbar buttons and TipTap extensions
Basic Usage
import RichTextEditor from '@myunisoft/design-system/RichTextEditor';
<RichTextEditor content="<p>Hello <strong>world</strong>!</p>" />
Props Examples
content - Initial HTML Content
The content prop sets the initial HTML content of the editor.
<RichTextEditor
content="<p>This is <strong>bold</strong> and <em>italic</em> text.</p>"
/>
placeholder - Placeholder Text
Display placeholder text when the editor is empty.
<RichTextEditor
content=""
placeholder="Commencez à rédiger ou tapez @ pour parcourir les balises"
/>
readOnly - Read-Only Mode
Use readOnly={true} to display content without allowing editing. The toolbar remains visible but all buttons are disabled.
<RichTextEditor
readOnly
content="<p>This content is <strong>read-only</strong> and cannot be edited.</p>"
/>
toolbarSet - Predefined Toolbar Configurations
Minimal Toolbar
Use toolbarSet="minimal" for a simplified toolbar with essential formatting options.
<RichTextEditor
toolbarSet="minimal"
content="<p>Editor with minimal toolbar</p>"
/>
Full Toolbar (default)
Use toolbarSet="all" (or omit the prop) for the complete toolbar.
<RichTextEditor
toolbarSet="all"
content="<p>Editor with full toolbar</p>"
/>
onUpdate - Change Handler
Use the onUpdate callback to react to content changes. This is inherited from TipTap's UseEditorOptions.
const [html, setHtml] = useState('<p>Edit me!</p>');
<RichTextEditor
content={html}
onUpdate={({ editor }) => {
console.log('Content changed:', editor.getHTML());
}}
/>
(start typing to see output)
ref - Accessing the Editor Instance
Use a ref to access the TipTap editor instance directly for programmatic control.
const editorRef = useRef(null);
<RichTextEditor
ref={editorRef}
content="<p>Click the button to insert text</p>"
/>
<Button onClick={() => {
editorRef.current?.editor?.commands.insertContent(' [Inserted!]');
}}>
Insert Text
</Button>
slotProps.toolbar.buttons - Custom Toolbar Buttons
Override the default toolbar with a custom set of buttons.
<RichTextEditor
content="<p>Custom toolbar with only bold, italic, and lists</p>"
slotProps={{
toolbar: {
buttons: ['bold', 'italic', '-', 'bulletList', 'orderedList']
}
}}
/>
slotProps.toolbar.extraButtons - Additional Toolbar Buttons
Append custom buttons to the default toolbar. You can add custom buttons with your own icons and actions.
<RichTextEditor
content="<p>Toolbar with an extra print button</p>"
slotProps={{
toolbar: {
extraButtons: [
'-',
{
id: 'print',
icon: <PrintRounded />,
ariaLabel: 'Print',
onClick: (editor) => {
window.print();
}
}
]
}
}}
/>
extensions - Custom TipTap Extensions
Add custom TipTap extensions to extend the editor's functionality. This allows you to add features like highlights, code blocks, mentions, and more.
import { Highlight } from '@tiptap/extension-highlight';
<RichTextEditor
content="<p>Text with <mark>highlighted</mark> content</p>"
extensions={[Highlight]}
/>
slotProps.toolbar.overflowMenu - Toolbar Overflow Behavior
Control whether overflow buttons collapse into a "more" menu (default) or wrap to the next line.
With Overflow Menu (default)
<RichTextEditor
content="<p>Resize the container to see overflow menu</p>"
slotProps={{
toolbar: { overflowMenu: true }
}}
/>
Without Overflow Menu (wrapping)
<RichTextEditor
content="<p>Buttons wrap to next line</p>"
slotProps={{
toolbar: { overflowMenu: false }
}}
/>
Available Toolbar Buttons
Use these button IDs in slotProps.toolbar.buttons or slotProps.toolbar.extraButtons:
| Button ID | Description |
|---|---|
'-' | Separator (vertical divider) |
'bold' | Bold text |
'italic' | Italic text |
'strikethrough' | Strikethrough text |
'underline' | Underlined text |
'fontColor' | Text color picker |
'subscript' | Subscript text |
'superscript' | Superscript text |
'alignment' | Alignment split button (left, center, right, justify) |
'alignLeft' | Align text left |
'alignCenter' | Center text |
'alignRight' | Align text right |
'alignJustify' | Justify text |
'orderedList' | Numbered list |
'bulletList' | Bullet list |
'indent' | Increase list indent |
'outdent' | Decrease list indent |
'blockquote' | Block quote |
'link' | Insert/edit hyperlink |
'image' | Insert image |
'table' | Insert table |
'undo' | Undo last action |
'redo' | Redo last action |
'clearFormatting' | Remove all formatting from selection |
Custom Button Example
Create a custom toolbar button with full control over its behavior:
const customButton = {
id: 'highlight',
icon: <HighlightIcon />,
ariaLabel: 'Highlight text',
onClick: (editor) => {
editor.chain().focus().toggleHighlight().run();
},
isActive: (editor) => editor.isActive('highlight'),
isDisabled: (editor) => !editor.can().toggleHighlight()
};
<RichTextEditor
content="<p>Select text and click highlight</p>"
slotProps={{
toolbar: {
extraButtons: ['-', customButton]
}
}}
/>
Props Reference
| Name | Type | Default | Description |
|---|---|---|---|
content | string | - | Initial HTML content |
placeholder | string | - | Placeholder text when editor is empty |
readOnly | boolean | false | Disable editing and toolbar buttons |
toolbarSet | 'all' | 'minimal' | 'all' | Predefined toolbar configuration |
extensions | AnyExtension[] | [] | Additional TipTap extensions |
onUpdate | (props: { editor: Editor }) => void | - | Callback fired on content change |
ref | Ref<RichTextEditorRef> | - | Ref to access editor instance |
slotProps.toolbar.buttons | ToolbarButton[] | - | Custom toolbar buttons (overrides toolbarSet) |
slotProps.toolbar.extraButtons | ToolbarButton[] | [] | Additional buttons appended to toolbar |
tags | TagDefinition[] | - | Tag definitions for mention support (type @ to insert) |
onTagSelect | OnTagSelect | - | Callback to intercept tag selection (return true to prevent default) |
slotProps.toolbar.overflowMenu | boolean | true | Collapse overflow into "more" menu |
The component also accepts all TipTap UseEditorOptions as props.
Dynamic Tags (Mentions)
The tags prop enables built-in mention support. Users type @ to insert a tag from a suggestion dropdown, and a toggle switch is automatically added to the toolbar to switch between tag names and resolved values.
tags— array of tag definitions ({ key, label, value? })key— identifier stored in HTML as<var name="key">label— display name shown in suggestions and in tag modevalue— resolved value shown in normal mode (optional, falls back tokey)- If
valueisnullorundefined, the tag chip renders empty (the tag is still present but displays nothing) - If
valueis0or an empty string"", the value is displayed as-is
- If
- Use
serializeTags(html)to convert the editor's HTML output back to<var name="...">elements for storage.
import RichTextEditor, { serializeTags } from '@myunisoft/design-system/RichTextEditor';
import type { TagDefinition } from '@myunisoft/design-system/RichTextEditor';
const tags: TagDefinition[] = [
{ key: 'nom_societe', label: 'raison sociale', value: 'MyUnisoft SA' },
{ key: 'date_fin', label: 'date clôture exercice', value: '31/12/2025' },
{ key: 'siret', label: 'numéro SIRET', value: null }, // renders empty
{ key: 'effectif', label: 'effectif moyen', value: undefined }, // renders empty
{ key: 'nb_mois', label: 'nombre de mois', value: 0 }, // renders "0"
];
<RichTextEditor
tags={tags}
content="<p>Bonjour <var name='nom_societe'></var></p>"
/>
// On save:
const html = editorRef.current?.editor?.getHTML();
const serialized = serializeTags(html);
// => "<p>Bonjour <var name="nom_societe"></var></p>"
onTagSelect - Custom Tag Selection Handler
Use onTagSelect to intercept tag selection and handle specific tags differently. The callback receives the selected tag and an insert function. Return true to prevent the default insertion (you handle it yourself via insert(id)), or false for normal behavior.
This is useful for tags that require user input before insertion, such as opening a modal to build a formula.
import RichTextEditor, { type OnTagSelect } from '@myunisoft/design-system/RichTextEditor';
const tags = [
{ key: 'nom_societe', label: 'raison sociale', value: 'MyUnisoft SA' },
{ key: 'formule_compte', label: 'formule compte' },
];
const handleTagSelect: OnTagSelect = (tag, insert) => {
if (tag.key === 'formule_compte') {
openAccountModal((formula) => insert(formula));
return true; // prevent default insertion
}
return false; // default behavior for other tags
};
<RichTextEditor tags={tags} onTagSelect={handleTagSelect} />
TypeScript Types
import type {
ToolbarButton,
BuiltInButtonId,
CustomToolbarButton,
TagDefinition
} from '@myunisoft/design-system/RichTextEditor';
import { serializeTags } from '@myunisoft/design-system/RichTextEditor';
// Built-in button IDs
type BuiltInButtonId =
| '-' | 'bold' | 'italic' | 'strikethrough' | 'underline'
| 'fontColor' | 'subscript' | 'superscript'
| 'alignment' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'alignJustify'
| 'orderedList' | 'bulletList' | 'indent' | 'outdent'
| 'blockquote' | 'link' | 'image' | 'table' | 'undo' | 'redo'
| 'clearFormatting';
// Custom button definition
type CustomToolbarButton = {
id: string;
icon: ReactNode;
onClick: (editor: Editor) => void;
isActive?: (editor: Editor) => boolean;
isDisabled?: (editor: Editor) => boolean;
ariaLabel?: string;
};
// Ref type
type RichTextEditorRef = {
editor: Editor | null;
};