Vivid Logo Vivid

Rich Text Editor

Lets users create and format styled text content, and embed rich media such as links and images.

Usage

The Rich Text Editor provides composable and customizable rich text editing features built on top of the ProseMirror library. Each feature adds specific functionality to the editor (e.g. formatting, lists, links) that you can individually enable and configure.

To use the Rich Text Editor, first create a configuration with the features you want to include.

import { RteConfig, RteBase, RteToolbarFeature, RteBoldFeature } from '@vonage/vivid';

const config = new RteConfig([new RteBase(), new RteToolbarFeature(), new RteBoldFeature()]);

See the features documentation for a list of available features.

Then, create an editor instance from the config, optionally with an initial document.

const instance = config.instantiateEditor({
	initialDocument: {
		/* ... */
	},
});

To render it, pass the instance to the Rich Text Editor component.

import { registerRichTextEditor } from '@vonage/vivid';

registerRichTextEditor('your-prefix');

const rteComponent = document.querySelector('your-prefix-rich-text-editor');
rteComponent.instance = instance;

Guide

Document Model

Documents are represented as JSON objects following the ProseMirror document model.

An example document could look like this:

{
	"type": "doc",
	"content": [
		{
			"type": "paragraph",
			"content": [
				{
					"type": "text",
					"text": "Hello"
				},
				{
					"type": "text",
					"text": " world!",
					"marks": [
						{
							"type": "bold"
						}
					]
				},
				{
					"type": "text",
					"text": "Click me",
					"marks": [
						{
							"type": "link",
							"attrs": {
								"href": "https://vonage.com"
							}
						}
					]
				},
				{
					"type": "inlineImage",
					"attrs": {
						"imageUrl": "/vonage.png",
						"alt": "Vonage Logo",
						"size": null,
						"natualWidth": 100,
						"naturalHeight": 50
					}
				}
			]
		}
	]
}

It is a tree structure of nodes similar to HTML. However, unlike HTML, markup like bold is attached to nodes as "marks". You can learn more about the format in the ProseMirror documentation.

The exact schema of which nodes and marks can be in the document depends on the features you have enabled. Each feature documents how it extends the document model.

A document is a single doc node and has the type RteDocument. Its children must be block nodes.

A part of a document is represented as an array of nodes and has the type RteFragment:

[{ "type": "text", "text": "Hello" }]

Persisting Documents

Since documents are JSON-serializable, they can be stored directly in a database or sent over the network. This allows loading them into the editor again or rendering them in different contexts.

The schema of a specific RteConfig will be stable across minor versions of Vivid. We will consider modifications to the schema as breaking changes in line with our Release Policy.

However, if you make changes to your RteConfig, you should ensure that the editor remains compatible with previously stored documents or migrate them accordingly.

Rendering Documents

To render documents inside a web application, you can use the Rich Text View component. It accepts a view that is created by RteConfig's instantiateView method.

It renders documents identically to how they appear in the editor. You can also customize the rendering of specific nodes or marks by providing the renderChildView option.

renderChildView?: (view: RteView) => { dom: HTMLElement; contentDom?: HTMLElement; } | true | false;

The function is called for each node and mark in the document. The kind is indicated by view.type: 'node' | 'mark', and view.node or view.mark will contain the respective JSON representation of the node/mark.

You can render a custom content for this node/mark by returning true and using the child scoped slot. The slot receives { view: RteView }.

If you return false, the view is rendered as normal.

To render the children of the view, render view.children using a nested VRichTextView. Since the nested view might also render using the scoped slot, you can set up a component that recursively renders itself.

renderChildView: (view) => {
	if (view.type === 'mark' && view.mark.type === 'bold') {
		return true; // uses `child` scoped slot to render
	}
	return false; // use default rendering
};
<!-- RichText.vue -->
<script setup lang="ts">
	import { VRichTextView } from '@vonage/vivid-vue';
	import type { RteView } from '@vonage/vivid';

	const { view } = defineProps<{
		view: RteView;
	}>();

	const onClick = () => {
		window.alert('Bold text clicked!');
	};
</script>
<template>
	<VRichTextView :view="view">
		<template #child="{ view }">
			<button @click="onClick" style="font-weight: bold">
				<RichText :view="view.children" />
			</button>
		</template>
	</VRichTextView>
</template>

You can render a custom HTML element for this node/mark by returning it as the dom property. If there is child content, Rich Text View will render them as children of contentDom, which defaults to dom.

If you return false, the view is rendered as normal.

HTML Conversion

Documents can be converted to and from HTML using the RteHtmlParser and RteHtmlSerializer classes:

import { RteHtmlParser, RteHtmlSerializer } from '@vonage/vivid';

const parser = new RteHtmlParser(config);
const doc = parser.parseDocument('<p>Hello <strong>World</strong></p>');
// -> { type: 'doc', content: [...] }
const frag = parser.parseFragment('<p>Hello <strong>World</strong></p>');
// -> { type: 'paragraph', content: [...] }

const serializer = new RteHtmlSerializer(config);
serializer.serializeDocument(doc); // -> '<p>Hello <strong>World</strong></p>'
serializer.serializeFragment(frag); // -> '<p>Hello <strong>World</strong></p>'

When parsing HTML, the input will be sanitized using the DOMPurify library to strip out potentially dangerous HTML.

The default parser will make a best-effort attempt to parse arbitrary HTML, ignoring unsupported tags and attributes. The default serializer attempts to produce idiomatic and widely compatible HTML that can be converted back into the same document.

Their exact behaviour and output is undefined and may change between minor versions.

It's guaranteed that parsing the output of the serializer will yield the original document, even across minor versions. If we make a change that break this guarantee, we will consider it a breaking change in line with our Release Policy.

Customizing HTML Conversion

You can provide a modifyDom function to manipulate the DOM before it is parsed or serialized:

const fragment = parser.parseFragment('<img data-attachment-id="1">', {
	modifyDom: (dom) => {
		for (const img of dom.querySelectorAll('img[data-attachment-id]').values()) {
			img.setAttribute('src', `attachment://${img.getAttribute('data-attachment-id')}`);
		}
	},
}); /* -> [{
	"type": "inlineImage",
	"attrs": {
		"imageUrl": "attachment://1",
		...
	},
}] */

serializer.serializeFragment(fragment, {
	modifyDom: (dom) => {
		for (const img of dom.querySelectorAll('img[data-src]').values()) {
			const url = new URL(img.getAttribute('data-src')!);
			img.setAttribute('data-attachment-id', url.hostname);
		}
	},
}); // -> '<img src="" data-src="attachment://1" alt="" data-attachment-id="1">'

You can also customize the ProseMirror parsing and serialization logic directly.

For parsing, the modifyParseRules function allows you to modify the ProseMirror ParseRules used:

const parser = new RteHtmlParser(config, {
	modifyParseRules: (rules) => {
		rules.nodes.paragraph.push({ tag: 'div.paragraph' });
		rules.marks.bold.push({ tag: 'span.bold' });
	},
});
parser.parseFragment("<div class='paragraph'><span class='bold'>Hello</span> world</div>"); // -> { type: 'paragraph', content: [...] }

For serialization, you can override the default serializers for nodes and marks. Serializers need to return a ProseMirror DOMOutputSpec:

const serializer = new RteHtmlSerializer(config, {
	serializers: {
		nodes: {
			paragraph: () => ['div', { class: 'paragraph' }, 0],
		},
		marks: {
			bold: () => ['span', { class: 'bold' }, 0],
		},
	},
});

Editor Instance API

The editor instance holds all the state and functionality of the editor, but does not render anything on its own. To display it, pass it to a Rich Text Editor component.

You can use the instance to get and modify the document programmatically.

Configuration Options

onChange

The onChange callback is called whenever the document changes.

const instance = config.instantiateEditor([], {
	onChange: () => {
		console.log('Document changed:', instance.getDocument());
	},
});

foreignHtmlParser / foreignHtmlSerializer

Users can copy or drag arbitrary HTML content in or out of the editor.

The editor uses the foreignHtmlParser and foreignHtmlSerializer to handle this content.

When not provided, it uses a default parser and serializer.

config.instantiateEditor({
	foreignHtmlParser: new RteHtmlParser(config, {
		/* ... */
	}),
	foreignHtmlSerializer: new RteHtmlSerializer(config, {
		/* ... */
	}),
});

Methods

getDocument

/**
 * Returns the current document state.
 */
getDocument(): RteDocument;

replaceSelection

/**
 * Replaces the current selection with the given content. If no text is selected, this inserts the content at the cursor position.
 */
replaceSelection(
	content: RteFragment,
	options?: {
		/**
		 * Controls where the cursor is placed after the replacement:
		 * - 'end': places the cursor at the end of the inserted content (default)
		 * - 'start': places the cursor at the start of the inserted content
		 */
		cursorPlacement?: 'end' | 'start';
		/**
		 * If true, selects the inserted content after replacement. Defaults to false.
		 */
		selectContent?: boolean;
	}
): void;

replaceDocument

/**
 * Replaces the entire document with the given new document.
 * Unlike reset, this preserves the rest of the editor state. The undo history is preserved, so the user can undo the replacement.
 */
replaceDocument(
	newDocument: RteDocument,
	options?: {
		/**
		 * Controls where the cursor is placed after the replacement:
		 * - 'start': places the cursor at the start of document (default)
		 * - 'end': places the cursor at the end of the document
		 */
		cursorPlacement?: 'start' | 'end';
		/**
		 * If true, selects the whole document after replacement. Defaults to false.
		 */
		selectContent?: boolean;
	}
): void;

reset

/**
 * Reset the editor to its initial state. Optionally, an initial document can be provided.
 */
reset(initialDocument?: RteDocument): void;

feature

Some features expose a run-time API that can be accessed via the feature method. See the documentation of each feature for details.

instance.feature(RteToolbarFeature).hidden = true;

Features

The Base feature is required for the editor to work. All other features are optional and can be combined as needed.

RteBase

Provides basic editing functionality, undo/redo functionality and enables basic text blocks. By default, only the paragraph block is enabled.

Configuration options:

  • heading1?: boolean: Add the heading1 (h1) node. Defaults to false.
  • heading2?: boolean: Add the heading2 (h2) node. Defaults to false.
  • heading3?: boolean: Add the heading3 (h3) node. Defaults to false.
  • paragraph?: boolean: Add the paragraph (<p>) node. Defaults to true.

Feature API:

  • disabled: boolean: Whether the editor is disabled. When disabled, user input is prevented and UI elements are disabled.

Keyboard shortcuts:

  • Undo: Ctrl + Z / Cmd + Z
  • Redo: Ctrl + Y / Cmd + Shift + Z
  • Convert to Paragraph: Ctrl + Alt + 0 / Cmd + Option + 0
  • Convert to Heading Level <X>: Ctrl + Alt + <X> / Cmd + Option + <X>
  • Create a New Block: Enter / Shift + Enter

To insert a hard break (<br>) use Shift + Enter with RteHardBreakFeature enabled.

block+ block inline* block inline* block inline* block inline*

RteToolbarFeature

Adds the toolbar to the editor. Features automatically add their controls to the toolbar.

Configuration options:

  • popupDirection?: 'inward' | 'outward': Whether tooltips and other popups prefer to be open towards or away from the main text-editing area. Defaults to 'inward'.

Feature API:

  • hidden: boolean: Whether the toolbar is hidden.

RtePlaceholderFeature

Adds placeholder text when the editor is empty. The placeholder is affected by the current text block and font size.

Example usage:

new RtePlaceholderFeature({ text: 'Start typing here...' });

Configuration options:

  • text: string (required): The placeholder text to display when the editor is empty.

RteCharacterCountFeature

Counts the number of characters in the document and optionally enforces a character limit.

When a limit is set, the editor blocks input that would exceed the limit. Pasted content is automatically truncated to fit within the remaining space.

The character count is based on the text content of the document. Block separators (e.g. between paragraphs) are not counted, but leaf nodes like hard breaks count as one character.

Example usage:

new RteCharacterCountFeature({ limit: 280 });

Configuration options:

  • limit?: number: Maximum number of characters allowed. When set, input that would exceed the limit is blocked and pasted content is truncated to fit. If not set, characters are counted but not limited.

Feature API:

  • characters: number (read-only): The current number of characters in the document.
  • limit: number | undefined (read-only): The configured character limit, or undefined if no limit is set.

RteHardBreakFeature

Allows inserting hard line breaks (<br>).

Keyboard shortcuts:

  • Insert Hard Break: Shift + Enter
inline

RteKeyboardShortcutsFeature

Lets you add or override keyboard shortcuts. Each instance needs a unique name (first argument). Shortcuts use ProseMirror key names (e.g. "Enter", "Shift-Enter", "Mod-b").

Example usage:

new RteKeyboardShortcutsFeature('escape-deselect', {
	shortcuts: {
		Escape: (rteInstance) => {
			return true;
		},
	},
});

Constructor: new RteKeyboardShortcutsFeature(id, options)

Configuration options:

  • id: string(required): Unique identifier for this feature instance.
  • options.shortcuts (record):
    Map of key name to KeyboardShortcutHandler. Keys use ProseMirror key names (e.g. "Enter", "Shift-Enter", "Mod-b").

KeyboardShortcutHandler:

  • A function that receives the RteInstance (rteInstance: RteInstance) => boolean. Return true to consume the key (prevent default); false to let other features handle it.

RteTextBlockPickerFeature

Provides a text block picker in the toolbar to change the current text block or range of selected blocks.

Example usage:

new RteTextBlockPickerFeature({
	options: [
		{ node: 'heading1', label: 'Heading 1' },
		{ node: 'heading2', label: 'Heading 2' },
		{ node: 'heading3', label: 'Heading 3' },
		{ node: 'paragraph', label: 'Paragraph' },
	],
});

Configuration options:

  • options: TextBlockOption[]: The options to show in the text block picker.

TextBlockOption:

  • node: string (required): Name of the block node.
  • label: string (required): Label to show in the picker.

RteFontSizePickerFeature

Adds a font size picker to the toolbar to change the font size of the selected text.

Example usage:

new RteFontSizeFeature({
	options: [
		{ size: '24px', label: 'Extra Large' },
		{ size: '18px', label: 'Large' },
		{ size: '14px', label: 'Normal' },
		{ size: '12px', label: 'Small' },
	],
	onBlocks: [{ node: 'heading1' }, { node: 'heading2' }, { node: 'paragraph', defaultSize: '14px' }],
});

Configuration options:

  • options: FontSizeOption[] (required): The available font sizes from largest to smallest. Note that different font sizes can occur in the document when external HTML is pasted / dragged in.
  • onBlocks?: FontSizeOnBlock[] (required): Which blocks the font sizes can be applied to. If not provided, the mark can be applied on all blocks.

FontSizeOption:

  • size: string (required): CSS font-size value (e.g., 12px, 1.5em, var(--font-size-large)).
  • label: string (required): Label for the font size option shown in the toolbar.

FontSizeOnBlock:

  • node: string (required): Name of the block node.
  • defaultSize?: string: Which option is selected by default on this block. If not provided, no option is selected. When font size changes from a different value back to the default, the editor will remove the mark.

Keyboard shortcuts:

  • Increase Font Size: Ctrl + Shift + . / Cmd + Shift + .
  • Decrease Font Size: Ctrl + Shift + , / Cmd + Shift + ,

Known issues:

  • Cursor size does not adjust correctly when the cursor is in the middle of text.

RteTextColorPickerFeature

Adds a text color picker to the toolbar to change the color of the selected text.

This feature adds a text-color-picker slot in which you need to place a Simple Color Picker.

Example usage:

new RteTextColorPickerFeature({
	onBlocks: [
		{ node: 'heading2' },
		{ node: 'paragraph', defaultColor: '#000000' },
	],
}),

Configuration options:

  • onBlocks?: TextColorOnBlock[]: Which blocks the text color can be applied to. If not provided, the mark can be applied on all blocks.

TextColorOnBlock:

  • node: string (required): Name of the block node.
  • defaultColor?: string: Which color is selected by default on this block. If not provided, no color is selected. When color changes from a different value back to the default, the editor will remove the mark.

When using the alternate theme the colors may no longer have sufficient contrast.

Text Style Features

The RteBoldFeature, RteItalicFeature, RteUnderlineFeature, RteStrikethroughFeature, and RteMonospaceFeature add the corresponding text styling options to the editor.

Configuration options:

  • onBlocks?: Array<{ node: string }>: The blocks on which the formatting can be applied. If not provided, the mark can be applied on all blocks.

Keyboard shortcuts:

  • Bold: Ctrl + B / Cmd + B
  • Italic: Ctrl + I / Cmd + I
  • Underline: Ctrl + U / Cmd + U
  • Strikethrough: Alt + Shift + 5 / Cmd + Shift + X
  • Monospace: Ctrl + Shift + M / Cmd + Shift + M

RteListFeature

Adds support for bullet and numbered lists.

Configuration options:

  • bulletList?: boolean: Enables bullet lists. Defaults to false.
  • numberedList?: boolean: Enables numbered lists. Defaults to false.

You must enable at least one list type.

Keyboard shortcuts:

  • Toggle bullet list: Ctrl + Shift + 8 / Cmd + Shift + 8
  • Toggle numbered list: Ctrl + Shift + 7 / Cmd + Shift + 7
  • Move to sub list: Tab
  • Move out of list: Shift + Tab
  • New list item: Enter
  • Exit list: Enter on empty list item
block list (listItem | list)+ block list (listItem | list)+ inline*

RteAlignmentFeature

Adds the ability to change the alignment of text blocks.

Keyboard shortcuts:

  • Align Left: Ctrl + Shift + L / Cmd + Shift + L
  • Align Center: Ctrl + Shift + E / Cmd + Shift + E
  • Align Right: Ctrl + Shift + R / Cmd + Shift + R

RteLinkFeature

Adds the ability to insert links. This features requires the RteToolbarFeature.

Keyboard shortcuts:

  • Insert link: Ctrl + K / Cmd + K
  • Pressing Space or Enter after typing or pasting a URL will automatically convert it into a link.

RteInlineImageFeature

Adds support for inline images. This feature does not provide any UI for adding images by itself, however the user can paste HTML content containing images into the editor.

inline

Image Size

When selecting an image, the editor will show options to change the size to one of the following sizes:

  • Original: null - natural dimensions
  • Fit: 100% - fit to the width of the editor
  • Small: <min(300px, 0.5 * naturalWidth)>

Image URL

To display an image, the editor needs a URL to use as the src attribute of the HTML <img> tag. For example, this could be one of the following:

  • HTTP URL: https://example.com/image.jpg or /api/attachments/12345
  • Blob URL: blob:https://example.com/abcd-efgh-ijkl-mnop
  • Data URL: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...

By default, the editor will use the imageUrl as the src attribute of the <img> tag.

However, storing src URL in the document is not always ideal, since it may need to change or may not be known at the time the image is inserted.

Instead, you can use a logical URL like attachment://12345 as imageUrl and resolve it to a src URL by providing mapping functions in the configuration:

resolveUrl

Type: resolveUrl: (imageUrl: string) => ResolvedUrl | AsyncGenerator<ResolvedUrl, ResolvedUrl> (optional)

The resolveUrl function is called whenever the editor needs to display an image. It receives the imageUrl value for the image and decides how to display this image.

Possible ResolvedUrl values:

  • string: Displays an image with this src URL.
  • null: Displays nothing.
  • { type: 'placeholder', create?: (slotName: string) => (() => unknown) | undefined }: Displays arbitrary slotted placeholder content. See Rendering Placeholders for details.

resolveUrl can also be an async generator of resolved values. This allows you to resolve the url asynchronously or update the displayed content over time.

Keep in mind that users can duplicate images inside the editor, so resolveUrl may be called multiple times for the same imageUrl.

Here is an example that resolves attachment://<id> URLs with placeholders for loading and error states:

new RteInlineImageFeature({
	resolveUrl: async function* (imageUrl) {
		const url = new URL(imageUrl);
		if (url.protocol !== 'attachment:') {
			return imageUrl; // regular src URL
		}

		const attachmentId = parseInt(url.host, 10);

		yield {
			type: 'placeholder',
			create: (slotName) => {
				/* display loading placeholder */
			},
		};

		try {
			return await getAttachmentUrlOnceUploaded(attachmentId);
		} catch (error) {
			return {
				type: 'placeholder',
				create: (slotName) => {
					/* display error placeholder */
				},
			};
		}
	},
});

serializeUrlToHtml

Type: serializeUrlToHtml: (imageUrl: string) => string | null

Called when the document is serialized to HTML. Note that this occurs not only when you use a HtmlSerializer, but also when content is copied or dragged out of the editor.

For each image, it is called with the imageUrl and should return the src value of <img> element. If it returns null, the image is omitted from HTML output.

When serializing an image to HTML, an invalid src like attachment://12345 may be dropped from the output due to sanitization. To preserve this value it is added as the data-src attribute as well.

parseUrlFromHtml

Type: parseUrlFromHtml: (src: string) => string | null} (optional)

Called when HTML is parsed. Note that this occurs not only when you use a HtmlParser, but also when HTML content is pasted or dragged into the editor.

For each HTML <img> tag, it is called with the src attribute and should return the imageUrl. If it returns null, the image is discarded.

The Rich Text View component will render images like serialized HTML. It will not invoke resolveUrl.

Rendering Placeholders

Resolving an imageUrl to a placeholder allows you to display custom slotted content in place of the image.

The placeholder content must be non-interactive.

When rendering a placeholder, the editor will create a slot with a unique name (e.g. inline-image-placeholder-1) and call the provided create function with this slot name.

You can optionally return a cleanup function from create, which will be called when the placeholder slot is removed.

Placeholders are never reused, so create and cleanup will be called at most once.

new RteInlineImageFeature({
	resolveUrl: (url: string) => ({
		type: 'placeholder',
		create: (slotName: string) => {
			const placeholder = document.createElement('div');
			placeholder.slot = slotName;
			richTextEditor.appendChild(placeholder);

			return () => {
				placeholder.remove();
			};
		},
	}),
});

Use the inline-image-placeholder scoped slot to render the placeholder content. The slot receives { url: string } as props, containing the image URL.

new RteInlineImageFeature({
	resolveUrl: (url: string) => ({
		type: 'placeholder',
	}),
});
<VRichTextEditor :instance="instance">
	<template #inline-image-placeholder="{ url }">
		<MyImagePlaceholder :url="url" />
	</template>
</VRichTextEditor>

Determining Used Images

You may want to know which images are still in use or detect when an image is deleted, for example to manage corresponding attachments.

In general, you cannot know when it is safe to remove the attachment since the user could restore the image from clipboard or history at a later time.

You can inspect the document to find referenced images when saving or submitting it, after which the editor should no longer be used.

RteFileHandlerFeature

Allows you to handle files dropped or pasted into the editor.

Configuration options:

  • handleFiles: (files: File[]) => RteFragment | Promise<RteFragment> | null (required)

Called when files are dropped or pasted into the editor. When pasting file, the returned fragment replaces the current selection. When dropping files, it is inserted at the drop location.

If it returns null, the files are ignored and the current selection remains unchanged.

The function is only called for files inserted into the editor content area. Files dropped or pasted into other areas (e.g. menu bar or content in the editor-end slot) are not handled.

RteDropHandlerFeature

Allows you to customize drag and drop behavior over the editor viewport.

Configuration options:

  • onViewportDragOver: (event: DragEvent) => boolean (optional)

Called whenever the user drags content over the editor viewport. If it returns true, the editor will ignore this content. No drop cursor will be displayed and handleFiles will not be called. Note that users can drag content around inside the editor, and HTML or text content in from outside the editor. This will also be disabled when returning true, so you should ensure that you only return true for content you want to handle, e.g. files.

Remember to call event.preventDefault() if you want to allow dropping.

  • onViewportDrop: (event: DragEvent) => void (optional)

Called whenever the user drops content over the editor viewport.

Remember to call event.preventDefault() to prevent the default browser behavior.

  • onViewportDragEnd: () => void (optional)

Called whenever dragging over the viewport ends, including after a drop.

In this example, we display a drop zone overlay when files are dragged over the editor viewport:

RteToolbarButtonFeature

Adds a button to the toolbar that performs an action when clicked.

Configuration

The RteToolbarButtonFeature constructor takes two arguments:

  1. id (string, required): A unique identifier for this toolbar button instance.
  2. options (object, required): Configuration options:
  • label: string (required) - The aria-label for the button.
  • icon: string (required) - The icon name for the button.
  • action: object (required) - The action to perform when the button is clicked.
  • order?: number - The order of the button in the toolbar. Lower numbers appear first. Buttons with the same order are sorted alphabetically by feature id.

Action types:

  • { type: 'insert-text', text: string } - Inserts the specified text at the cursor position.
new RteToolbarButtonFeature('mention', {
	label: 'Mention user',
	icon: 'user-line',
	action: { type: 'insert-text', text: '@' },
});

RteAtomFeature

Adds support for custom inline atom nodes. Atoms are non-editable inline elements that can be used for things like variables, mentions or tags.

Usage

You can create multiple RteAtomFeature instances with different atom names to support different atom types.

Atoms have no content and a single required attribute value:

new RteAtomFeature('mention');

const mention = { type: 'mention', attrs: { value: 'John' } };

By default, the atom value is rendered as plain text. Like other nodes, you can style atoms using CSS parts. See Styling for more information.

You can customize the rendered content by providing the resolveValue configuration option:

  • resolveValue?: (value: string) => string | null | AsyncGenerator<string, string>
new RteAtomFeature('tag', {
	resolveValue: (value) => `#${value}`,
});

resolveValue can also return an async generator, which allows you to fetch the result asynchronously or update it over time:

new RteAtomFeature('mention', {
	resolveValue: async function* (userId) {
		yield 'Loading...';
		const user = await fetchUser(userId);
		return `@${user.name}`;
	},
});

const mention = { type: 'mention', attrs: { value: '1' } };

HTML Serialization

Atoms are serialized to HTML as <span> elements with data-atom-type and data-value attributes:

<span data-atom-type="mention" data-value="john">john</span>

You can customize the text of the span by providing the serializeValueToHtml configuration option:

  • serializeValueToHtml?: (value: string) => string | null: Customize how the atom value is serialized to HTML. If it returns null, the atom is omitted from HTML output.
inline

RteInputRuleFeature

Enables text replacement rules that automatically transform text as you type. Use this for features like converting text patterns to emojis, hashtags, or other content.

Configuration

The RteInputRuleFeature constructor takes two arguments:

  1. id (string, required): A unique identifier for this input rule instance.
  2. options (object, required): Configuration options:
  • pattern: RegExp (required) - The regex to match against text before the cursor. Do not add a trailing $.
  • handler: (match: string[]) => RteFragment | null (required) - Called when the pattern matches. Return an RteFragment to replace the match, or null to skip.
  • matchAfterWhitespace?: boolean - When true, the rule triggers only on space or Enter after the match.

RteSuggestFeature

Enables regex-based suggestions for implementing features like variables ({name}), mentions (@user), tags (#topic) or emojis (:smile:). The feature watches for text patterns before the cursor and can either auto-replace them or show a suggestions popover.

This feature works well together with RteAtomFeature - use RteSuggestFeature to trigger the suggestion UI and insert atoms into the document.

Configuration

The RteSuggestFeature constructor takes two arguments:

  1. id (string, required): A unique identifier for this suggest feature instance.
  2. options (object, required): Configuration options:
  • pattern: RegExp (required) - The regex to match against text before the cursor. The regex should end with $ to match at cursor position.
  • load: (match: string[]) => Suggestion[] | Promise<Suggestion[]> (required) - Called when regex matches. Receives the full match array including capture groups. The returned suggestions are displayed in a popover. If a promise is returned, a loading indicator is shown.
  • select: (suggestion: Suggestion) => RteFragment (required) - Called when a suggestion is selected. The matched text is replaced by the returned RteFragment.

The Suggestion type has the following properties:

interface Suggestion {
	text: string;
	textSecondary?: string;
	data?: unknown;
}

You can customize the empty state message when no suggestions are found by providing the <id>-suggestions-empty slot:

<vwc-rich-text-editor>
	<div slot="mention-suggestions-empty">No users found</div>
</vwc-rich-text-editor>

You can customize the empty state message when no suggestions are found with the suggestions-empty scoped slot. The slot receives { id: string } as props, where id is the feature ID (e.g., "mention"):

<VRichTextEditor :instance="instance">
	<template #suggestions-empty="{ id }">
		<span v-if="id === 'mention'">No users found</span>
		<span v-else-if="id === 'emoji'">No emojis found</span>
	</template>
</VRichTextEditor>

Keyboard shortcuts:

  • Navigate between suggestions: Arrow Up / Arrow Down
  • Select the highlighted suggestion: Enter
  • Close suggestions popover: Escape

Styling

The basic text blocks heading-<level> and paragraph are exposed as a shadow DOM part, which allows you to customize its styling using CSS.

This applies to both the Rich Text Editor and Rich Text View components, so styling can be shared.

CSS Variables

Editor Padding

Use --rich-text-editor-padding-inline and --rich-text-editor-padding-block to control the padding around the editor content. These override the values used by --editor-padding-inline and --editor-padding-block (e.g. for slotted content alignment).

  • --rich-text-editor-padding-inline: Default 16px
  • --rich-text-editor-padding-block: Default 8px

Slots

editor-start / editor-end

Content placed in these slots is displayed at the start or end of the scrollable editor area.

You can use the --editor-padding-inline and --editor-padding-block CSS variables to match the padding of the editor content.

Content with position of sticky / fixed / absolute is positioned relative to the editor viewport:

status

Content placed in this slot is displayed between the editor viewport and the toolbar.

Properties

editorViewportElement

The editorViewportElement property provides access to the scrollable editor viewport element. You can use this to determine the scroll position.

API Reference

Properties

Property Type Default Description
instance @vonage/vivid#RteInstance The editor instance created from the RteConfig. Without it, the editor will not render.
Property Type Default Description
instance
(property only)
@vonage/vivid#RteInstance The editor instance created from the RteConfig. Without it, the editor will not render.

Slots

Name Description
editor-end Displayed at the end of the scrollable editor area.
editor-start Displayed at the start of the scrollable editor area.
inline-image-placeholder Placeholder content for inline images. (dynamic slot props)
status Displayed between the editor viewport and the toolbar.
suggestions-empty Empty state if no suggestions are found. (dynamic slot props)
text-color-picker Color picker for the RteTextColorFeature.
Scoped slots trigger internal slottable-request handling in wrappers.
Name Description
editor-end Displayed at the end of the scrollable editor area.
editor-start Displayed at the start of the scrollable editor area.
inline-image-placeholder Placeholder content for inline images.
status Displayed between the editor viewport and the toolbar.
suggestions-empty Empty state if no suggestions are found.
text-color-picker Color picker for the RteTextColorFeature.