Presets
On this page
@better-rich-text/presets has four ready-made presets for createRichTextEditor. Each one lives at its own subpath, so an editor only bundles the preset it imports and the plugins in it.
| Preset | Import | For | Size (gzip) |
|---|---|---|---|
documentPreset |
@better-rich-text/presets/document |
Long documents, like Word or Google Docs | 391 KiB |
createCollaborationPreset(options) |
@better-rich-text/presets/collaboration |
Review with comments, suggested edits and history | 228 KiB |
createEmailPreset(options) |
@better-rich-text/presets/email |
Newsletters and transactional email | 198 KiB |
fullPreset |
@better-rich-text/presets/full |
Demos and evaluation: everything that needs no setup | 455 KiB |
Sizes are a whole editor (engine, view, interface, skin and plugins) built by node scripts/size.mjs, which checks them against their budgets. The standard preset is 161 KiB for comparison.
The core minimal and standard presets stay as they are. These presets are not in the presets map that createRichTextEditor looks names up in, because that would put every plugin in every bundle. Pass the preset object instead:
import { createRichTextEditor } from '@better-rich-text/core';
import { documentPreset } from '@better-rich-text/presets/document';
const editor = createRichTextEditor('#editor', { preset: documentPreset });
editor.commands.footnote.insert({ text: 'Source: annual report.' });The package index re-exports all four presets. It has no side effects, so a bundler still drops the ones you do not use, but the subpaths make the choice obvious.
A preset is { id, plugins, toolbar, features }. Each one is declared as const satisfies Preset, so the command catalogue is inferred from its plugins: editor.commands.footnote.insert type-checks with the document preset and is a compile error with the email preset.
The demo is apps/playground/presets.html. It switches between the presets and lists each one's plugins, features and toolbar.
Document
The standard preset plus:
| Area | Plugins | Toolbar and menus |
|---|---|---|
| Structure | plugin-anchor, plugin-footnotes, plugin-toc, plugin-details, plugin-callout, plugin-page-break |
footnote toc callout details pageBreak; anchors in Insert |
| Code and math | plugin-math, plugin-code-highlight with JavaScript, TypeScript, HTML, CSS, JSON, Python, shell and SQL |
math codeBlock; the language bar on each code block |
| Files | plugin-documents: Word import and export, PDF, print, document properties |
documents; File menu |
| History | plugin-revisions with the in-memory store |
history |
| Quality | plugin-a11y-check, plugin-link-check (offline checks) |
a11yCheck linkCheck; Tools menu |
| Markdown | plugin-markdown: paste, input rules, source view (Mod-Shift-M) |
markdown |
| Content | plugin-datetime, plugin-emoji, plugin-styles, plugin-templates, plugin-image-editor |
dateTime styles templates; Edit image in the image context menu |
| Blocks | plugin-block-handle |
A handle beside each block to drag it or open the block menu; Alt+Shift+Up and Down (Block handles) |
Interface features: the menu bar (file edit view insert format tools table help), context menu, help (Alt+0), preview, visual aids (View menu), and the features of each plugin above.
Defaults it sets up for you:
Styles: Lead, Small print and Note, exported as
<p class="lead">,<p class="small-print">and<p class="note">.documentStylesFeatureadds their CSS in thebrtcascade layer, so your own rules win.DOCUMENT_CONTENT_CSShas the same rules for your published pages.Templates: a read-only Starter category with Meeting notes, Project brief and Letter.
{{date}}and{{user.name}}are filled in when inserted.Revision history: versions are kept in memory for the editor's lifetime. Pass a persistent store to keep them (see Trimming and extending).
The toolbar runs in the default overflow mode: groups that do not fit move behind the More button, and everything else (fonts, anchors, visual aids, print, Word import) is in the menu bar and the context menu. DOCUMENT_TOOLBAR holds the string.
getMarkdown() and setMarkdown() need the Markdown codec, which is an editor option rather than a plugin:
import { markdown } from '@better-rich-text/plugin-markdown';
createRichTextEditor('#editor', { preset: documentPreset, markdown });Collaboration
The standard preset plus the user directory, @mentions, comments with the margin sidebar, suggested edits and revision history. It is a factory because the user directory needs your lookup and the current user.
import { createRichTextEditor } from '@better-rich-text/core';
import { createCollaborationPreset } from '@better-rich-text/presets/collaboration';
const preset = createCollaborationPreset({
users: {
currentUserId: me.id,
fetchUsers: (ids, signal) => api.users(ids, { signal }),
searchUsers: (query, signal) => api.searchUsers(query, { signal }),
},
});
const editor = createRichTextEditor('#editor', { preset, actorId: me.id });
editor.commands.suggestion.setMode({ enabled: true });| Option | Default | |
|---|---|---|
users |
required | { currentUserId, fetchUsers, searchUsers?, users? } or a UserDirectory. See Users. |
mentions |
@ searches the directory |
createMentionsPlugin options, for example extra # triggers. |
comments |
{ sidebar: 'margin' } |
createCommentsFeature options, or false to leave comments out. |
suggestions |
true |
false leaves suggested edits out. |
revisions |
in-memory store | createRevisionsPlugin options, or false to leave history out. |
collab |
none | The real-time plugin, collabPlugin. See below. |
The toolbar has the usual editing groups, then comment comments, suggest acceptSuggestion rejectSuggestion previousSuggestion nextSuggestion reviewView and history. Items for features you turn off are left out, and the command catalogue follows: with comments: false, editor.commands.comment is a compile error. collaborationToolbar(options) returns the toolbar for a set of options.
Real-time editing
The preset never imports @better-rich-text/collab-client, so the client is only bundled when you add it. Pass collabPlugin to the preset, then connect a client to the editor:
import { CollabClient, CollaboratorsBar, PresenceLayer, WebSocketTransport, collabPlugin } from '@better-rich-text/collab-client';
const preset = createCollaborationPreset({ users, collab: collabPlugin });
const editor = createRichTextEditor('#editor', { preset, actorId: me.id });
const client = new CollabClient(editor.editor, {
documentId: 'report-42',
transport: new WebSocketTransport({ url: `wss://collab.example.com/${encodeURIComponent('report-42')}` }),
token: session.token,
});
const presence = new PresenceLayer(editor.view, client); // remote carets and selections
new CollaboratorsBar(client, presence, document.querySelector('#people')!); // who is hereThe server must install the same plugins in the same order, because the schema hash has to match. Build the preset the same way on the server and pass its plugins to createCollabServer (Server):
const { plugins } = createCollaborationPreset({ users: { currentUserId: 'server', fetchUsers: async () => [] }, collab: collabPlugin });
await createCollabServer({ plugins, authenticate, storage });IndexedDbOutbox keeps unsent changes across reloads (outbox and outboxKey options of CollabClient), and LocalAuthority runs the authority in the page for tests and demos. apps/playground/collab.html shows both.
Text formatting, links, lists, images, tables, alignment, colours and fonts, merge tags with preview, and templates. Code blocks, math, embeds, footnotes, details and anything else email clients cannot render are not installed.
import { createRichTextEditor } from '@better-rich-text/core';
import { createEmailPreset, exportEmailHtml } from '@better-rich-text/presets/email';
const editor = createRichTextEditor('#editor', {
preset: createEmailPreset({
fields: [
{ key: 'first_name', label: 'First name', group: 'Contact', sample: 'Robin' },
{ key: 'plan', label: 'Plan', group: 'Account', sample: 'Pro' },
],
}),
});
const { html, text, issues } = exportEmailHtml(editor, { preheader: 'News for March', baseUrl: 'https://example.com/' });| Option | Default | |
|---|---|---|
fields |
DEFAULT_EMAIL_FIELDS |
Merge fields for the mergeTags menu and {{ autocomplete: first name, last name, email, company and unsubscribe link. |
strictFields |
true |
Reject {{keys}} that are not in fields. |
templates |
emailTemplates |
A TemplateSource or a list of templates. The defaults are Newsletter, Announcement and Receipt, written with the default fields. |
exportEmailHtml(editor, options?) takes a RichTextEditor or an engine Editor and runs exportEmail from @better-rich-text/format-email: CSS inlined, a centred 600px table layout, absolute URLs, and a plain-text alternative. Merge tags stay as {{key}} for your sending service to fill. issues lists each conversion that changed something. The options are the EmailExportOptions in Documents; layout: 'none' with fragment: true returns just the content.
createRichTextEditor always bundles the standard plugins because it can look up 'standard' by name, so the code block plugin is in the bundle even though the email preset does not install it.
Full
The document preset plus comments (margin sidebar), suggested edits, merge tags that accept any {{key}}, media embeds, proofing with the browser's spellchecker, autocorrect and counts (plugin-limits).
Left out because they need your services: the user directory, real-time collaboration, the file manager (plugin-assets), the AI assistant (plugin-ai), saving (plugin-save) and @mention sources. Add them with plugins, or use the collaboration preset.
import { fullPreset } from '@better-rich-text/presets/full';
createRichTextEditor('#editor', { preset: fullPreset, actorId: 'demo' });Trimming and extending
A preset is plain data, so you can build your own from one. Spread its parts and change what you need:
import type { Preset } from '@better-rich-text/core';
import { documentPreset } from '@better-rich-text/presets/document';
import { createRevisionsPlugin } from '@better-rich-text/plugin-revisions';
import { indexedDbRevisionStore } from '@better-rich-text/revisions';
// Trim: no Markdown, no emoji.
const leanDocument: Preset = {
...documentPreset,
id: 'lean-document',
plugins: [...documentPreset.plugins.filter((p) => p.id !== 'markdown' && p.id !== 'emoji')],
features: documentPreset.features.filter((f) => f.id !== 'markdown' && f.id !== 'emoji'),
toolbar: documentPreset.toolbar.replace(' markdown', ''),
};
// Extend: keep versions in IndexedDB instead of memory.
createRichTextEditor('#editor', {
preset: {
...documentPreset,
plugins: [
...documentPreset.plugins.filter((p) => p.id !== 'revisions'),
createRevisionsPlugin({ store: indexedDbRevisionStore(), documentId: 'report-42' }),
],
},
});For a single extra plugin, the plugins option adds it after the preset's, and toolbar and features replace the preset's:
createRichTextEditor('#editor', {
preset: documentPreset,
plugins: [createAssetsPlugin({ source: httpAssetSource({ baseUrl: '/api/assets' }) })],
features: [...documentPreset.features, assetsFeature],
toolbar: `${documentPreset.toolbar} | assets`,
});Each preset module also exports its parts, so you can recombine them: documentPlugins, documentFeatures and DOCUMENT_TOOLBAR; fullPlugins, fullFeatures and FULL_TOOLBAR; EMAIL_TOOLBAR, DEFAULT_EMAIL_FIELDS and emailTemplates.
Typed commands
Inference needs the plugin list as a tuple. filter returns a plain array, so a filtered preset still type-checks but loses the exact catalogue: typed as Preset, it gives the untyped catalogue. To keep inference in a custom preset, list the plugins yourself and declare it as const satisfies Preset:
import { standardPreset, type Preset } from '@better-rich-text/core';
import { footnotesPlugin } from '@better-rich-text/plugin-footnotes';
import { mathFeature, mathPlugin } from '@better-rich-text/plugin-math';
export const notesPreset = {
id: 'notes',
plugins: [...standardPreset.plugins, footnotesPlugin, mathPlugin],
toolbar: `${standardPreset.toolbar} | footnote math`,
features: [...standardPreset.features, mathFeature],
} as const satisfies Preset;