Getting started
Frameworks
Matra is a document model and a view that attaches to an element. It has no opinion about what renders around it, so every framework already works — the only question is how much sugar there is.
Four have bindings
| Package | What you get | |
|---|---|---|
| React | @matrajs/react | useEditor, useEditorState, useEditorFocus, EditorContent |
| Vue | @matrajs/vue | useEditor, EditorContent |
| Svelte | @matrajs/svelte | matra — a use: action, the editor, and a store |
| Solid | @matrajs/solid | createMatra — the editor, a ref and a signal |
All four are MIT, all four are under a hundred lines, and none re-implements anything. They exist for two reasons: turning the editor's changes into a re-render is the one piece of boilerplate worth removing, and mounting twice into one element is the mistake everybody makes once. Each binding guards it.
Everything else is three calls
Create it, mount it into an element, destroy it when the element goes away.
import { createEditor, starterKit } from '@matrajs/core'
const editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
editor.mount(document.querySelector('#editor'))
// later
editor.destroy()
That is the whole of it. destroy() removes the listeners and the
contenteditable attribute, so the element is safe to reuse — which is what a hot
reload, a route change and a recycled list row all rely on.
The guard worth copying is editor.unsafe.view. It is truthy once mounted, so
a component that renders twice does not attach a second view to one element. Every binding
uses it and so should yours.
Svelte
An action, which is exactly the shape Svelte has for this. Stores rather than runes, so the same package works in Svelte 4 and 5.
<script>
import { starterKit } from '@matrajs/core'
import { matra } from '@matrajs/svelte'
const { action, editor, state } = matra({
extensions: starterKit,
content: '<p>Hello</p>',
})
</script>
<button onclick={() => editor.commands.toggleBold()}
aria-pressed={$state.isActive('bold')}>Bold</button>
<div use:action></div> Solid
A signal rather than an external store · Solid's reactivity is not a render loop, so
everything reading state() re-runs and nothing else does.
import { starterKit } from '@matrajs/core'
import { createMatra } from '@matrajs/solid'
export function Editor() {
const { editor, mount, state } = createMatra({
extensions: starterKit,
content: '<p>Hello</p>',
})
return (
<>
<button onClick={() => editor.commands.toggleBold()}
aria-pressed={state().isActive('bold')}>Bold</button>
<div ref={mount} />
</>
)
} Angular
A directive, so the element stays in the template where it belongs. There is no
@matrajs/angular package, and the reason is worth saying rather than leaving you
to wonder: an Angular library has to be published through ng-packagr in Angular's
partial-compilation format, or it fails in every consumer's AOT build. Shipping one that breaks
in production builds would be worse than these fifteen lines, which
your compiler handles correctly because it compiles them.
import { Directive, ElementRef, OnDestroy, OnInit, inject } from '@angular/core'
import { createEditor, starterKit } from '@matrajs/core'
@Directive({ selector: '[matra]', standalone: true, exportAs: 'matra' })
export class MatraDirective implements OnInit, OnDestroy {
private host = inject(ElementRef<HTMLElement>)
editor = createEditor({ extensions: starterKit, content: '<p>Hello</p>' })
ngOnInit() {
if (!this.editor.unsafe.view) this.editor.mount(this.host.nativeElement)
}
ngOnDestroy() {
this.editor.destroy()
}
}
// <div matra #ed="matra"></div>
// <button (click)="ed.editor.commands.toggleBold()">Bold</button> Qwik, Astro, Lit, or no framework
The same three calls, wherever you can get an element and a teardown hook. In Astro or a
plain page, a <script> tag is enough — every editor on this site is exactly
that.
Keeping a toolbar in step
The one thing worth knowing without a binding: editor.on('change') and
editor.on('selectionChange') both return an unsubscribe function, and
editor.isActive('bold') reads the state from the document. That is everything a toolbar
needs, in any framework:
const off = editor.on('selectionChange', () => {
boldButton.setAttribute('aria-pressed', String(editor.isActive('bold')))
})
// on teardown
off() Server rendering
createEditor needs no DOM. Build a document, read
getHTML() or getJSON(), and send it — the view only exists once
something calls mount. That is also why
toMarkdown runs on a server with no jsdom in sight.
content: '<p>…</p>' needs a browser
· content: storedJson and content: fromMarkdown(text) do not. Writing
HTML out with getHTML() works either way.
The paid packages, anywhere
@matrajs/ai, @matrajs/collab and
@matrajs/versions are extensions, not components. They go in the same array and know
nothing about what renders around them, in every framework on this page.