---
title: Quick start
description: Build a working Edytor editor with the bundled plugins, an initial document, change handling and your own styles.
icon: zap
---

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.

1. **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.

2. **Give it a document**

    Pass the initial content as `value`, a JSON tree of blocks.

3. **Read the changes**

    Use `onChange`, or bind the editor instance with `bind:edytor` and read `edytor.value`.

4. **Style it**

    Import the Notion theme, or target the `data-edytor-*` attributes the editor renders.

## The editor

```svelte title="src/lib/Editor.svelte" check
<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 (<kbd>Mod</kbd>+<kbd>B</kbd>, <kbd>Mod</kbd>+<kbd>Alt</kbd>+<kbd>1</kbd>, …) and HTML import rules. List, to-do and toggle items continue on <kbd>Enter</kbd>. |
| `imagePlugin` (default) | An image block: embed from a link (or your upload, with `createImagePlugin({ upload })`), with an editable caption. |
| `arrowMovePlugin` (default) | <kbd>Mod</kbd>+<kbd>Shift</kbd>+<kbd>↑</kbd> / <kbd>↓</kbd> move the caret's block; <kbd>Mod</kbd>+<kbd>↑</kbd> / <kbd>↓</kbd> 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](/docs/plugins#default-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](/docs/customization/menus).

## 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`:

```svelte
<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](/docs/editor/commands).

## Style the editor

The Notion theme above styles every bundled kind; its colors and fonts are custom properties you can override (see [Styling](/docs/customization/styling#notion-theme)). 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:

```css title="src/app.css"
.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](/docs/editor/edytor-component#snippets)) or define your own kind in [Custom blocks](/docs/customization/blocks).

## Next steps

**[The Edytor component](/docs/editor/edytor-component)**

Every prop, with types and defaults.

**[Editing programmatically](/docs/editor/commands)**

Insert text, change blocks and move them from code.

**[Collaboration](/docs/collaboration)**

Share one document between tabs, devices and people.

**[Plugins](/docs/plugins)**

What each bundled plugin does and how to write one.
