In the box
Nodes, marks and behaviour are the same shape: a plain object with a name. Each group below is a real editor with those extensions in it and nothing else.
Marks
Bold, italic, strike, code, underline, highlight. Each is a plain object with a command, a key and a tag — and yours would be the same shape.
import { bold, italic, strike, code, underline, highlight, link } from '@matrajs/core' Blocks
Every block type carries its own markdown shortcut. Start a line with # or > or ``` and the input rule does the rest — no menu required.
import { heading, blockquote, codeBlock, horizontalRule, paragraph } from '@matrajs/core' Lists
Tab nests, Shift-Tab lifts back out, Enter on an empty item leaves the list. The checkbox is a real input the caret cannot get inside — tick one.
import { bulletList, orderedList, listItem, taskList, taskItem } from '@matrajs/core' Tables
Insert one and type into it. Header rows, colspan and column widths survive a round trip through JSON and back.
import { table, tableRow, tableCell, tableHeader, image } from '@matrajs/core' Writing aids
Type "quotes" and they curl the right way round. Two hyphens become an em dash, three dots an ellipsis, -> an arrow. Undo groups by word, not by keystroke.
import { typography, placeholder, characterCount, history, textAlign } from '@matrajs/core' Templates
Put the caret in the first paragraph and try to type: it is locked, and the lock is a change filter, so a paste and a drag are refused the same way. Type {{city}} for a field, then fill them all.
import { locked, field, snippets } from '@matrajs/core' Layout
Two columns from one paragraph and back again without losing a word. Show the spaces and paragraph ends, which are drawn rather than stored. Select a word and change its case; bold stays bold.
import { columnList, column, invisibleCharacters, textTransform } from '@matrajs/core' Paid
None of these is a button you can press on a page, so none of them is drawn as one. The core stays MIT whatever happens to them.
01
AInpm i @matrajs/ai Streaming edits that survive concurrent typing. A stream that arrives while somebody is still typing lands where it was meant to, because the positions are mapped rather than remembered.
02
Collaborationnpm i @matrajs/collab An authority, step rebasing and remote cursors. Two people editing the same paragraph converge on the same document rather than on the last write.
03
Version historynpm i @matrajs/versions Snapshots, a real diff between them, and restore as one undo step. Blocks are paired on their content, so a paragraph that moved is the same paragraph rather than a deletion and an insertion at two unrelated places.
Table of contents, unique block ids, drag handle and comments are the four that other editors put behind a paywall. They are in the core here, and they stay there. What is paid is the work that is genuinely hard: streaming into a document somebody else is typing in, converging two of them, and answering what changed between two drafts.
What you ship
Every extension you leave out really does leave the bundle. It is also worth knowing how little that saves: the engine is the floor and the extensions are rounding.
Gzipped, bundled with esbuild, measured by pnpm size. Dropping from the
starter kit to two marks saves
1.8 kB — because an extension costs about
0.20 kB and the engine underneath is 29.1 kB. Tiptap splitting
into a package per extension gets their minimum to
86.1 kB, which is still 2.8× Matra carrying everything,
because ProseMirror is underneath it and is not optional.
The whole directory
The command, the shortcut, and the line you would actually write. Copy an import and it works — there is nothing else to install.
Inline formatting. Each one is a command, a key and a tag.
editor.commands.toggleBold() editor.commands.toggleItalic() editor.commands.toggleStrike() editor.commands.toggleCode() editor.commands.toggleUnderline() editor.commands.toggleHighlight('yellow') editor.commands.setLink({ href: 'https://matrajs.com' }) editor.commands.toggleSubscript() editor.commands.toggleSuperscript() editor.commands.toggleKbd() editor.commands.setColor('#c00') editor.commands.addComment('thread-1') Block types. Every one carries its own markdown shortcut.
extensions: [document, paragraph, text] editor.commands.setParagraph() extensions: [text] editor.commands.toggleHeading(2) editor.commands.toggleBlockquote() editor.commands.toggleCodeBlock() editor.commands.insertHorizontalRule() editor.commands.insertHardBreak() editor.commands.insertImage({ src: '/cat.png' }) editor.commands.toggleCallout('warning') editor.commands.insertDetails() // with ...detailsKit extensions: [...detailsKit] editor.commands.insertYoutube({ src: 'https://youtu.be/…' }) editor.commands.insertEmbed('https://player.vimeo.com/video/1') editor.commands.insertPageBreak() editor.commands.setColumns(3) // with ...columnsKit editor.commands.addColumn() editor.commands.insertBlockMath('E = mc^2') // with ...mathKit() Tab nests, Shift-Tab lifts, Enter on an empty item leaves the list.
editor.commands.toggleBulletList() editor.commands.toggleOrderedList() extensions: [listItem] editor.commands.toggleTaskList() editor.commands.toggleTaskItem() The four that other editors put behind a paywall, and tables.
editor.commands.insertTable(3, 3) editor.commands.addRowAfter() editor.commands.addColumnAfter() editor.commands.toggleHeaderRow() editor.extensionState('tableOfContents') uniqueId({ types: ['paragraph', 'heading'] }) editor.commands.moveBlock(from, to) focus({ className: 'has-focus' }) trailingNode() fileHandler({ accept: ['image/'], onDrop }) editor.commands.lock() editor.commands.fillFields({ name: 'Ada' }) editor.commands.setImageWidth(320, pos) editor.commands.insertFootnote() // with ...footnotesKit() editor.commands.goToFootnote(id) editor.commands.removeFootnote(id) The ones you never press. They work while you type.
placeholder({ text: 'Write something…' }) characterCount({ limit: 280 }) editor.commands.setTextAlign('center') editor.commands.indent() extensions: [typography] emoji({ emoticons: true }) autolink() editor.commands.clearFormatting() editor.commands.setSearch({ query: 'colour' }) codeHighlight({ highlight }) editor.commands.undo() smartPaste() editor.commands.sentenceCase() selectionHighlight({ wholeWord: true }) editor.commands.toggleInvisibleCharacters() editor.commands.setTextDirection('rtl') editor.commands.setLineHeight(1.5) typewriter({ position: 0.5 }) autosave({ save: (doc) => put(doc) }) snippets([{ trigger: 'sig', content: '— Nahim' }]) hashtag() editor.commands.insertInlineMath('x^2') Something watching what you type, and offering.
suggestion({ char: '/', name: 'slash' }) editor.commands.insertMention({ id: 'nahim' }) bubbleMenu({ element }) floatingMenu({ element }) ghostText({ suggest }) editor.commands.startDictation() Functions rather than extensions · nothing to add to the array.
toMarkdown(editor.getJSON()) editor.setContent(fromMarkdown('# Hello')) commentRanges(editor.getJSON()) fillFieldsIn(doc, values) // plain JSON in, plain JSON out hashtagsIn(editor.getJSON()) createEditor({ extensions: [...] }) Paid. Streaming edits that survive concurrent typing.
editor.commands.askAi('make this shorter') Paid. An authority, step rebasing and remote cursors.
collab({ clientId: 'a1' }) remoteCursors() Paid. Snapshots, a real diff between them, and restore.
editor.commands.snapshotVersion() diffDocs(before, after)