In the box

Everything hanging
off the core.

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.

79extensions, MIT
3paid packages
4that other editors charge for

Marks

The everyday six.

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.

  • bold
  • italic
  • strike
  • code
  • underline
  • highlight
  • link
import { bold, italic, strike, code, underline, highlight, link } from '@matrajs/core'
marks.md

Blocks

Headings, quotes, fences.

Every block type carries its own markdown shortcut. Start a line with # or > or ``` and the input rule does the rest — no menu required.

  • heading
  • blockquote
  • codeBlock
  • horizontalRule
  • paragraph
import { heading, blockquote, codeBlock, horizontalRule, paragraph } from '@matrajs/core'
blocks.md

Lists

Including the one with boxes.

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.

  • bulletList
  • orderedList
  • listItem
  • taskList
  • taskItem
import { bulletList, orderedList, listItem, taskList, taskItem } from '@matrajs/core'
lists.md

Tables

Rows, columns, and a caret.

Insert one and type into it. Header rows, colspan and column widths survive a round trip through JSON and back.

  • table
  • tableRow
  • tableCell
  • tableHeader
  • image
import { table, tableRow, tableCell, tableHeader, image } from '@matrajs/core'
tables.md

Writing aids

The ones you never press.

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.

  • typography
  • placeholder
  • characterCount
  • history
  • textAlign
import { typography, placeholder, characterCount, history, textAlign } from '@matrajs/core'
writing.md

Templates

Clauses that stay, blanks that fill.

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.

  • locked
  • field
  • snippets
import { locked, field, snippets } from '@matrajs/core'
templates.md

Layout

Columns, marks, and the case of things.

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.

  • columnList
  • column
  • invisibleCharacters
  • textTransform
import { columnList, column, invisibleCharacters, textTransform } from '@matrajs/core'
layout.md

Paid

Three packages, one subscription.

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.

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

Take five, take everything.

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.

The engine on its own createEditor, with an empty array
29.1 kB
Bold and underline document, paragraph, text, bold, underline
29.3 kB
The everyday six every mark, and nothing that makes a block
29.6 kB
The starter kit what most applications start from
31.1 kB
Starter kit, tables and checklists more than most pages need
33.7 kB
Tiptap, bold and underline only their per-package way, same bundler
86.1 kB

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

84 extensions,
every one of them here.

The command, the shortcut, and the line you would actually write. Copy an import and it works — there is nothing else to install.

Marks

@matrajs/core

Inline formatting. Each one is a command, a key and a tag.

bold Bold
editor.commands.toggleBold()
Mod-B
italic Italic
editor.commands.toggleItalic()
Mod-I
strike Struck through
editor.commands.toggleStrike()
Mod-Shift-X
code Inline code
editor.commands.toggleCode()
Mod-E
underline Underlined
editor.commands.toggleUnderline()
Mod-U
highlight Highlighted, with a colour attribute
editor.commands.toggleHighlight('yellow')
Mod-Shift-H
link A link, with the scheme checked before it reaches the DOM
editor.commands.setLink({ href: 'https://matrajs.com' })
subscript Subscript
editor.commands.toggleSubscript()
superscript Superscript
editor.commands.toggleSuperscript()
kbd A key name, as <kbd> · "press Ctrl"
editor.commands.toggleKbd()
Mod-Alt-K
textStyle Colour, background, font family and size · one mark, four attributes
editor.commands.setColor('#c00')
comment A thread anchored to a range, which survives edits around it
editor.commands.addComment('thread-1')

Blocks

@matrajs/core

Block types. Every one carries its own markdown shortcut.

document The root node. Every schema needs one
extensions: [document, paragraph, text]
paragraph The default block
editor.commands.setParagraph()
text The text node itself
extensions: [text]
heading Levels one to six
editor.commands.toggleHeading(2)
Mod-Alt-1 … 6 ##
blockquote A quote, which holds blocks rather than text
editor.commands.toggleBlockquote()
Mod-Shift-B >
codeBlock A fenced block that takes no marks
editor.commands.toggleCodeBlock()
Mod-Alt-C ```
horizontalRule A divider · one position, no insides
editor.commands.insertHorizontalRule()
---
hardBreak A line break inside a block
editor.commands.insertHardBreak()
Shift-Enter
image An image, with the src checked like a link
editor.commands.insertImage({ src: '/cat.png' })
callout A Notion-style callout, with a type and an emoji
editor.commands.toggleCallout('warning')
details A collapsible toggle, rendered as a real <details>
editor.commands.insertDetails() // with ...detailsKit
detailsSummary The title line of a toggle · Enter moves into the content
extensions: [...detailsKit]
Enter
youtube A video embed, built from the id on the privacy domain
editor.commands.insertYoutube({ src: 'https://youtu.be/…' })
embed Any embed page in a sandboxed frame, from an allowlist of hosts
editor.commands.insertEmbed('https://player.vimeo.com/video/1')
pageBreak A labelled line on screen, a new page in print
editor.commands.insertPageBreak()
columnList Two to six columns side by side · the block at the caret becomes the first
editor.commands.setColumns(3) // with ...columnsKit
column One column, which holds blocks
editor.commands.addColumn()
mathBlock A display formula · KaTeX, MathJax or a <code> of the source
editor.commands.insertBlockMath('E = mc^2') // with ...mathKit()
$$E=mc^2$$

Lists

@matrajs/core

Tab nests, Shift-Tab lifts, Enter on an empty item leaves the list.

bulletList Bulleted
editor.commands.toggleBulletList()
Mod-Shift-8 -
orderedList Numbered
editor.commands.toggleOrderedList()
Mod-Shift-9 1.
listItem What a list holds
extensions: [listItem]
taskList Checkboxes
editor.commands.toggleTaskList()
Mod-Shift-7 []
taskItem One checkbox · a real input the caret cannot enter
editor.commands.toggleTaskItem()
Mod-Enter

Structure

@matrajs/core

The four that other editors put behind a paywall, and tables.

table Rows, columns, header cells and column widths · Tab moves between cells
editor.commands.insertTable(3, 3)
Tab
tableRow One row
editor.commands.addRowAfter()
tableCell One cell, which holds blocks · a spanning cell widens rather than splits
editor.commands.addColumnAfter()
tableHeader A header cell
editor.commands.toggleHeaderRow()
tableOfContents Headings, with their level and position
editor.extensionState('tableOfContents')
uniqueId A stable id on every block, for comments and anchors
uniqueId({ types: ['paragraph', 'heading'] })
dragHandle Drag a block to reorder it
editor.commands.moveBlock(from, to)
focus A class on the block the caret is in
focus({ className: 'has-focus' })
trailingNode Always a paragraph after whatever ends the document
trailingNode()
fileHandler Files dropped or pasted, with a marker that survives the upload
fileHandler({ accept: ['image/'], onDrop })
locked Blocks that refuse every change · a template with fixed clauses
editor.commands.lock()
field A blank in a template · fill it in the editor, or in JSON on a server
editor.commands.fillFields({ name: 'Ada' })
{{name}}
imageResize A drag handle on every image, and a width the HTML keeps
editor.commands.setImageWidth(320, pos)
footnoteRef A footnote marker, numbered by where it stands
editor.commands.insertFootnote() // with ...footnotesKit()
footnote The note itself, under the text
editor.commands.goToFootnote(id)
footnotes The list the notes live in, kept last
editor.commands.removeFootnote(id)

Writing

@matrajs/core

The ones you never press. They work while you type.

placeholder Text in an empty block
placeholder({ text: 'Write something…' })
characterCount Characters and words, reduced on every change
characterCount({ limit: 280 })
textAlign Left, centre, right on a block
editor.commands.setTextAlign('center')
Mod-Shift-E
indent A paragraph or heading moved in and out, a level at a time
editor.commands.indent()
Tab
typography Curled quotes, em dashes, ellipses, arrows
extensions: [typography]
--
emoji Shortcodes as you type, and a table for a picker
emoji({ emoticons: true })
:tada:
autolink A URL becomes a link once a space follows it, or when pasted
autolink()
https://…
clearFormatting Every mark off, every block a paragraph · one undo step
editor.commands.clearFormatting()
Mod-\
search Find and replace · typing rescans one paragraph, not the document
editor.commands.setSearch({ query: 'colour' })
codeHighlight Syntax colours as decorations · built-in tokeniser, or Shiki, Prism, lowlight
codeHighlight({ highlight })
history Undo that groups by word, not by keystroke
editor.commands.undo()
Mod-Z
smartPaste Tab-separated text becomes a table, Markdown becomes blocks
smartPaste()
textTransform Upper, lower, title and sentence case on the selection or the word
editor.commands.sentenceCase()
selectionHighlight Every other occurrence of the selected word
selectionHighlight({ wholeWord: true })
invisibleCharacters A dot on every space, a pilcrow on every block · drawn, never stored
editor.commands.toggleInvisibleCharacters()
textDirection dir on a block, and right-to-left detected from the text
editor.commands.setTextDirection('rtl')
lineHeight Line height on a block, as a checked style
editor.commands.setLineHeight(1.5)
typewriter The line being written stays put; the page moves under it
typewriter({ position: 0.5 })
autosave Saves once typing pauses, and before the page goes away
autosave({ save: (doc) => put(doc) })
snippets Words that expand as they are typed · text, nodes or whole blocks
snippets([{ trigger: 'sig', content: '— Nahim' }])
sig
hashtag A tag as a node, listable from the JSON
hashtag()
#word
mathInline A formula in the run of text
editor.commands.insertInlineMath('x^2')
$x^2$

Menus

@matrajs/core

Something watching what you type, and offering.

suggestion The machinery behind a slash menu or an @ mention
suggestion({ char: '/', name: 'slash' })
/
mention A mention node · one position, its own DOM
editor.commands.insertMention({ id: 'nahim' })
@
bubbleMenu Your element, shown over the selection
bubbleMenu({ element })
floatingMenu Your element, shown on an empty line
floatingMenu({ element })
ghostText Inline completion · grey text after the caret, Tab to take it
ghostText({ suggest })
Tab
dictation Speak, and the words arrive at the caret · the browser’s own recogniser
editor.commands.startDictation()

Helpers

@matrajs/core

Functions rather than extensions · nothing to add to the array.

toMarkdown A document to markdown, without touching a DOM
toMarkdown(editor.getJSON())
fromMarkdown And back again
editor.setContent(fromMarkdown('# Hello'))
commentRanges Every thread in a document, with the range it still covers
commentRanges(editor.getJSON())
fillFieldsIn A mail merge on JSON, with no editor and no DOM
fillFieldsIn(doc, values) // plain JSON in, plain JSON out
hashtagsIn Every tag in a document, each once
hashtagsIn(editor.getJSON())
createEditor The editor itself
createEditor({ extensions: [...] })

AI

@matrajs/ai

Paid. Streaming edits that survive concurrent typing.

ai A streaming rewrite that lands where it was meant to
editor.commands.askAi('make this shorter')

Collaboration

@matrajs/collab

Paid. An authority, step rebasing and remote cursors.

collab Rebase local work over what arrived first
collab({ clientId: 'a1' })
remoteCursors Other people’s carets, mapped through your edits
remoteCursors()

Version history

@matrajs/versions

Paid. Snapshots, a real diff between them, and restore.

versions Snapshot, preview a diff, restore in one undo step
editor.commands.snapshotVersion()
diffDocs The diff on its own · works on any two documents
diffDocs(before, after)