Better Rich Text Buy licence

Presets

On this page
  1. Document
  2. Collaboration
  3. Real-time editing
  4. Email
  5. Full
  6. Trimming and extending
  7. Typed commands

@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.

PresetImportForSize (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:

TypeScript
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:

AreaPluginsToolbar 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">. documentStylesFeature adds their CSS in the brt cascade layer, so your own rules win. DOCUMENT_CONTENT_CSS has 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:

TypeScript
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.

TypeScript
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 });
OptionDefault
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:

TypeScript
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 here

The 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):

JavaScript
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.

Email

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.

TypeScript
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/' });
OptionDefault
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.

TypeScript
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:

TypeScript
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:

TypeScript
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:

TypeScript
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;