Frameworks
On this page
Every adapter wraps the same createRichTextEditor and behaves the same way:
The value has an explicit
format:html(a string) orjson(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 (
onChangein React and Solid,v-model,bind:value, Angular forms, Aureliavalue) only receive local edits. Thechangeevent receives every content change. Its payload hasvalue,origin(input,ui,history,api, ...),external,revision,previousRevision,docChangedandselection.readOnlykeeps content selectable and focusable.disabledalso marks the surfacearia-disabledand 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
windowordocument, 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
node path/to/better-rich-text-1.0.0/setup.mjs react
npm install react react-domimport { 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
node path/to/better-rich-text-1.0.0/setup.mjs vue
npm install 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
node path/to/better-rich-text-1.0.0/setup.mjs angular
npm install@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
node path/to/better-rich-text-1.0.0/setup.mjs svelte
npm install 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
node path/to/better-rich-text-1.0.0/setup.mjs aurelia
npm install @aurelia/kernel @aurelia/runtime-htmlimport Aurelia from 'aurelia';
import { BetterRichTextCustomElement } from '@better-rich-text/aurelia';
Aurelia.register(BetterRichTextCustomElement).app(MyApp).start();<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
node path/to/better-rich-text-1.0.0/setup.mjs solid
npm install solid-jsimport { 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
node path/to/better-rich-text-1.0.0/setup.mjs web-component
npm install<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
node path/to/better-rich-text-1.0.0/setup.mjs legacy
npm installimport $ 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).