Quick start
Build a working Edytor editor with the bundled plugins, an initial document, change handling and your own styles.
This page builds a complete Notion-style editor: rich text blocks and marks, images, code blocks, markdown shortcuts, a slash menu, a selection toolbar, block handles with a block menu, and the Notion theme. It then reads the document as it changes and styles the result.
Pick the plugins
Every block kind and mark comes from a plugin. <Edytor> already includes rich text (paragraphs, headings, lists, to-dos, toggles, callouts, quotes, dividers and the text marks), images and keyboard block moves. Add the others for menus, code and shortcuts.
Give it a document
Pass the initial content as value, a JSON tree of blocks.
Read the changes
Use onChange, or bind the editor instance with bind:edytor and read edytor.value.
Style it
Import the Notion theme, or target the data-edytor-* attributes the editor renders.
The editor
<script lang="ts">
import {
Edytor,
blockMenuPlugin,
codePlugin,
markdownShortcutsPlugin,
richTextPlaceholder,
slashMenuPlugin,
toolbarPlugin,
type EdytorInstance,
type JSONBlock,
type JSONDoc
} from 'edytor';
import 'edytor/themes/notion.css';
// Rich text, images and arrow moves are included by default.
const plugins = [codePlugin, markdownShortcutsPlugin, slashMenuPlugin, toolbarPlugin, blockMenuPlugin];
const value: JSONDoc = {
children: [
{ type: 'heading', data: { level: 'h1' }, content: [{ text: 'Meeting notes' }] },
{
type: 'paragraph',
content: [
{ text: 'Type ' },
{ text: '/', marks: { code: true } },
{ text: ' to turn a line into another block.' }
]
},
{ type: 'todo-item', data: { checked: false }, content: [{ text: 'Send the recap' }] }
]
};
let edytor = $state<EdytorInstance>();
const onChange = (root: JSONBlock) => {
console.log('changed', root.children);
};
</script>
<div class="edytor-notion">
<Edytor {plugins} {value} {onChange} bind:edytor class="editor" placeholder={richTextPlaceholder} />
</div>
The edytor-notion class applies the imported theme, and richTextPlaceholder shows Notion’s placeholders (“Heading 1”, “To-do”, “Type ‘/’ for commands”…). Block handles are on by default, so each block has a + and a drag grip without an extra plugin; blockMenuPlugin opens its menu when you click the grip. Pass blockHandles={false} to remove them.
What each plugin adds:
| Plugin | Adds |
|---|---|
richTextPlugin (default) |
The rich text block kinds and marks, their keyboard shortcuts (Mod+B, Mod+Alt+1, …) and HTML import rules. List, to-do and toggle items continue on Enter. |
imagePlugin (default) |
An image block: embed from a link (or your upload, with createImagePlugin({ upload })), with an editable caption. |
arrowMovePlugin (default) |
Mod+Shift+↑ / ↓ move the caret’s block; Mod+↑ / ↓ move selected blocks. |
codePlugin |
A code block (an island of codeLine children) with syntax highlighting. |
markdownShortcutsPlugin |
Typing # , - , 1. , [] , " , > , --- or ``` at the start of a block converts it; **bold**, *italic* and friends format inline. |
slashMenuPlugin |
A menu of block kinds, opened by typing /. |
toolbarPlugin |
A floating toolbar over a text selection: Turn into, link, marks and colors. |
blockMenuPlugin |
The menu of a block handle’s grip: Turn into, Duplicate, Move, Delete. |
Plugin order matters: when two plugins define the same block kind, mark or command, the first one wins. Rich text is added after your plugins so they can override it; defaultPlugins={false} turns the defaults off. See Plugins.
The slash menu, the toolbar, the block menu and the block handles each take a snippet to render your own markup while keeping their behavior. See Menus and handles.
Read the document
onChange receives the whole document after every committed change: local edits, remote edits and undo. Its argument is the root block, { type: 'root', children }.
With the instance bound, edytor.value returns the same JSON and is reactive, so it works in $derived:
<script lang="ts">
// …the editor from above
const blockCount = $derived(edytor?.value.children?.length ?? 0);
</script>
<p>{blockCount} blocks</p>
edytor.value returns the same object until the next change. Clone it before you mutate it. Computing it serializes the whole document, so prefer onChange (or a debounced save) over reading it in many places.
value is the initial content only. Updating the prop later does not replace the document, and value is not bindable (bind:value fails type-checking). To change content after mount, use commands.
Style the editor
The Notion theme above styles every bundled kind; its colors and fonts are custom properties you can override (see Styling). To style the editor yourself instead, drop the import and the edytor-notion class.
The editor renders a div with data-edytor (and your class). Each block is an element with data attributes you can target:
| Selector | Matches |
|---|---|
[data-edytor] |
The editable root. |
[data-edytor-block] |
Every block element. |
[data-edytor-type="heading"] |
Blocks of one kind. data-edytor-id holds the block id. |
[data-edytor-selected] |
Blocks in a block selection. |
[data-edytor-focused] |
Blocks the text selection is in. |
[data-edytor-void] |
Void blocks, and chrome marked non-editable. |
[data-edytor-text] |
Text segments. data-edytor-text-empty="true" when empty. |
[data-edytor-mark="bold"] |
A mark element (<strong>, <em>, <a>, …). |
[data-edytor-inline-block] |
Inline blocks. |
[data-placeholder]::before |
The placeholder of an empty block. |
[data-edytor-overlay] |
The layer after the root that holds handles, menus and remote carets. |
The block element’s tag comes from its kind: a heading renders h1–h3, a quote renders blockquote, list items render li. A few styles to start from:
.editor {
outline: none;
line-height: 1.6;
--edytor-drop-indicator-color: #2eaadc;
}
.editor p {
margin: 0;
}
.editor [data-edytor-type='quote'] {
border-left: 3px solid currentColor;
padding-left: 12px;
}
.editor [data-edytor-selected] {
background: #e8e6df;
border-radius: 3px;
}
The placeholder is drawn by a ::before rule the library ships, at 45% opacity. Override [data-edytor-text][data-placeholder]::before to restyle it.
To change a kind’s markup rather than its styles, override its snippet (see the component page) or define your own kind in Custom blocks.