Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Next steps

Was this page helpful?