Skip to main content

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());
}}
/>
HTML Output:
(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 IDDescription
'-'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

NameTypeDefaultDescription
contentstring-Initial HTML content
placeholderstring-Placeholder text when editor is empty
readOnlybooleanfalseDisable editing and toolbar buttons
toolbarSet'all' | 'minimal''all'Predefined toolbar configuration
extensionsAnyExtension[][]Additional TipTap extensions
onUpdate(props: { editor: Editor }) => void-Callback fired on content change
refRef<RichTextEditorRef>-Ref to access editor instance
slotProps.toolbar.buttonsToolbarButton[]-Custom toolbar buttons (overrides toolbarSet)
slotProps.toolbar.extraButtonsToolbarButton[][]Additional buttons appended to toolbar
tagsTagDefinition[]-Tag definitions for mention support (type @ to insert)
onTagSelectOnTagSelect-Callback to intercept tag selection (return true to prevent default)
slotProps.toolbar.overflowMenubooleantrueCollapse 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 mode
    • value — resolved value shown in normal mode (optional, falls back to key)
      • If value is null or undefined, the tag chip renders empty (the tag is still present but displays nothing)
      • If value is 0 or an empty string "", the value is displayed as-is
  • 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;
};