Better Rich Text Buy licence

Frameworks

On this page
  1. React
  2. Vue
  3. Angular
  4. Svelte
  5. Aurelia 2
  6. Solid
  7. Web Component
  8. Legacy hosts

Every adapter wraps the same createRichTextEditor and behaves the same way:

  • The value has an explicit format: html (a string) or json (a document envelope).

  • A value the editor just emitted, passed back in, does nothing. A different value replaces the document in one transaction with origin api, recorded in undo history by default (replaceHistory: 'clear' drops history instead).

  • Two-way channels (onChange in React and Solid, v-model, bind:value, Angular forms, Aurelia value) only receive local edits. The change event receives every content change. Its payload has value, origin (input, ui, history, api, ...), external, revision, previousRevision, docChanged and selection.

  • readOnly keeps content selectable and focusable. disabled also marks the surface aria-disabled and drops it from form submission.

  • Preset, plugins, config, toolbar, mode and locale are fixed when the editor is created. Placeholder, theme, read-only, disabled and format can change while it is mounted.

  • The editor is created in the framework's mount hook and destroyed exactly once on unmount. Importing an adapter in Node never touches window or document, and server rendering outputs an empty host element.

Install the packages from your download as described in Installation.

The shared logic lives in @better-rich-text/bindings (createBinding), which is also usable directly.

React

Shell
node path/to/better-rich-text-1.0.0/setup.mjs react
npm install react react-dom
TSX
import { useState } from 'react';
import { RichTextEditor, useCommand } from '@better-rich-text/react';

function BoldButton() {
  const bold = useCommand('format.toggleBold');
  return <button disabled={!bold.state.enabled} aria-pressed={bold.state.active === true} onClick={() => bold.run()}>Bold</button>;
}

export function Post() {
  const [html, setHtml] = useState('<p>Hello</p>');
  return (
    <RichTextEditor value={html} onChange={setHtml} placeholder="Write..." preset="minimal">
      <BoldButton />
    </RichTextEditor>
  );
}

useEditorState(selector) reads derived state through useSyncExternalStore and rerenders only when the selected value changes. useRichTextEditor(options) returns { ref, editor, binding } for a custom host. Children render inside the editor context. StrictMode's double mount creates and destroys one extra editor; only one stays mounted. To change preset, plugins or config, change the component's key.

Vue

Shell
node path/to/better-rich-text-1.0.0/setup.mjs vue
npm install vue
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { RichTextEditor } from '@better-rich-text/vue';

const body = ref('<p>Hello</p>');
</script>

<template>
  <RichTextEditor v-model="body" format="html" placeholder="Write..." @change="(c) => console.log(c.origin)" />
</template>

The component is written with defineComponent and h, so it needs no SFC build. useEditorState(selector) and useCommand(name) work in slot content or with an explicit editor ref. Changing preset, plugins or config rebuilds the editor and keeps its content; an inline object with the same contents does not.

Angular

Shell
node path/to/better-rich-text-1.0.0/setup.mjs angular
npm install
TypeScript
@Component({
  selector: 'app-post',
  imports: [RichTextEditorComponent, ReactiveFormsModule],
  template: `<better-rich-text [formControl]="body" required [maxLength]="5000" placeholder="Write..." />`,
})
export class PostComponent {
  body = new FormControl('<p></p>');
}

The component is standalone and OnPush, and implements ControlValueAccessor and Validator. required treats an empty paragraph as empty. maxLength counts visible characters. A disabled control disables the editor and readOnly is a separate input. The package is built with ng-packagr and its own TypeScript 6; see packages/angular/README.md.

Svelte

Shell
node path/to/better-rich-text-1.0.0/setup.mjs svelte
npm install svelte
Svelte
<script lang="ts">
  import { RichTextEditor, editorState, type RichTextEditorInstance } from '@better-rich-text/svelte';
  import { writable } from 'svelte/store';

  let body = $state('<p>Hello</p>');
  const editor = writable<RichTextEditorInstance | null>(null);
  const words = editorState(editor, (state, e) => e.schema.textBetweenBlocks(state.root, ' ').split(/\s+/).filter(Boolean).length);
</script>

<RichTextEditor bind:value={body} bind:editor={$editor} placeholder="Write..." />
<p>{$words ?? 0} words</p>

The .svelte source ships under the svelte export condition. use:richText={options} mounts an editor on any element, and commandState(editor, name) is a readable store of a command's state. Both are also exported from @better-rich-text/svelte/action, which needs no Svelte compiler. Hold the editor instance with $state.raw or a store, not $state, so it is not proxied.

Aurelia 2

Shell
node path/to/better-rich-text-1.0.0/setup.mjs aurelia
npm install @aurelia/kernel @aurelia/runtime-html
TypeScript
import Aurelia from 'aurelia';
import { BetterRichTextCustomElement } from '@better-rich-text/aurelia';

Aurelia.register(BetterRichTextCustomElement).app(MyApp).start();
HTML
<better-rich-text
  value.bind="body & validate"
  read-only.bind="locked"
  placeholder="Write..."
  config.bind="{ preset: 'minimal' }"
  editor.from-view="editor"
  change.trigger="onChange($event.detail)">
</better-rich-text>

value and document (a JSON envelope) bind two-way by default. config takes typed creation options; a config with different contents rebuilds the editor and keeps its content. The editor mounts in attached and is destroyed in detaching. Focus leaving the content bubbles as focusout, the default trigger of & validate, and the element also dispatches its own blur event. Because an empty paragraph is not an empty string, use .satisfies((v) => !isBlankValue(v)) instead of .required().

Solid

Shell
node path/to/better-rich-text-1.0.0/setup.mjs solid
npm install solid-js
TSX
import { createSignal } from 'solid-js';
import { RichTextEditor, createEditorSignal } from '@better-rich-text/solid';

function Count() {
  const blocks = createEditorSignal((state) => state.root.children.length);
  return <span>{blocks() ?? 0} blocks</span>;
}

export function Post() {
  const [html, setHtml] = createSignal('<p>Hello</p>');
  return (
    <RichTextEditor value={html()} onChange={(value) => setHtml(value)} placeholder="Write...">
      <Count />
    </RichTextEditor>
  );
}

The component builds its host element in code (the package itself uses no JSX) and Solid never renders inside it. createEditorSignal(selector) and createCommandSignal(name) notify only when their value changes. On the server the component renders nothing.

Web Component

Shell
node path/to/better-rich-text-1.0.0/setup.mjs web-component
npm install
HTML
<script type="module">
  import '@better-rich-text/web-component/define';
  const editor = document.querySelector('better-rich-text');
  editor.config = { preset: 'minimal', toolbar: 'bold italic | link' };
  editor.addEventListener('change', (event) => console.log(event.detail.origin, event.detail.value));
</script>

<form>
  <better-rich-text name="body" required placeholder="Write..." value="<p>Hello</p>"></better-rich-text>
</form>

Attributes: placeholder, readonly, disabled, name, preset, required, format, theme, value (initial value) and shadow. Properties: value, config, format, editor and the attribute mirrors. change, ready, destroy, focus, blur and error are composed CustomEvents. The element is form-associated through ElementInternals (form value, required validity, reset, restore) and falls back to a hidden input where that API is missing. Content renders in light DOM unless shadow is set before the element connects. defineBetterRichText(tag?, registry?) is idempotent and returns null where custom elements are unavailable. Moving the element keeps its editor; removing it destroys the editor.

Legacy hosts

Shell
node path/to/better-rich-text-1.0.0/setup.mjs legacy
npm install
JavaScript
import $ from 'jquery';
import { registerJQuery, bridge } from '@better-rich-text/legacy';

registerJQuery($);
$('#body').betterRichText({ placeholder: 'Write...' }).on('brt:change', (event, change) => save(change.value));
$('#body').betterRichText('value', '<p>Replaced</p>');

const b = bridge(document.querySelector('#notes'));
b.on('change', (change) => console.log(change.value));
b.destroy();

jQuery is passed in, never imported. bridge(element, options) returns getValue, setValue, set, on, focus and destroy. On a <textarea> or <input>, the editor mounts after the field, hides it and keeps its value in sync, firing native input and change events. Knockout's value binding and plain form posts keep working. To push a model change back into the editor, call bridge.setValue(textarea.value).