---
title: The Edytor component
description: Every prop of the Edytor component, with types and defaults, plus the snippet overrides for blocks, marks and inline blocks.
icon: square-pen
---

`<Edytor>` renders one editable (or readonly) view of a document. This page lists every prop it accepts and the snippets you can pass to replace how a kind renders.

```svelte
<script lang="ts">
	import { Edytor } from 'edytor';
</script>

<Edytor
	value={{ children: [{ type: 'paragraph', content: [{ text: 'Hello' }] }] }}
	placeholder="Write something…"
	onChange={(root) => save(root.children)}
/>
```

## Props

Most props are read once, when the component is created. Changing them later has no effect; recreate the component (for example with `{#key}`) to apply new ones. The **Live** column marks the props that follow updates.

| Prop | Type | Default | Live | Description |
| --- | --- | --- | --- | --- |
| `plugins` | `Plugin[]` | `[]` | | Block kinds, marks, inline blocks, hotkeys and hooks. Order matters: the first definition of a type wins. The view adds the rich text, image and arrow move plugins unless the list has them. See [Plugins](/docs/plugins#default-plugins). |
| `defaultPlugins` | `boolean` | `true` | | `false` renders exactly `plugins`, without the default rich text, image and arrow move plugins. Block handles still follow `blockHandles`. |
| `value` | `JSONDoc` | `{ children: [] }` | | The initial content. Used only to seed a document that has none; an empty value seeds one paragraph. See [Document model](/docs/concepts/document-model). |
| `document` | `EdytorDocument` | | | A document created with `createDocument`, `loadDocument` or `attachDocument`. Several views can share one. The view does not destroy it on unmount. |
| `server` | `string` | | | The sync server's base URL (`wss://…/rooms`). With `room`, the view joins `<server>/<room>` over a WebSocket and keeps a local copy. Read once. See [WebSocket](/docs/collaboration/websocket). |
| `room` | `string` | | | The document's id, 1 to 256 characters of any kind. Alone, it names a local IndexedDB copy; with `server`, the room to join (sent percent-encoded as one path segment). A URL collapses `.` and `..` and cannot encode a lone surrogate, so those ids, an empty one and one over 256 characters are never dialed: an editable view reports them through `onSyncRefused` as `4400` `invalid document id`, and a readonly view renders its `value`. Read once. |
| `params` | `Record<string, string>` | | Next dial | Query parameters sent with each connection, such as an auth token. |
| `onSyncExpired` | `({ reason, attempts, nextRetryMs }) => void` | | Yes | With `server`: the room closed the connection with `4401` (expired credentials). Pass a fresh token in `params` before the redial, due in `nextRetryMs`. Until a dial gets in, an empty document is not seeded. If the callback throws, the error is logged and the redial still happens. See [WebSocket](/docs/collaboration/websocket#options). |
| `onSyncRefused` | `(refusal: SyncRefusedError) => void` | | Yes | The server refused a provider of the view's document for good (`4403`, `4409`, `1008`…): called at mount for a refusal already standing, then for each new one. A callback that throws is logged and still hears the next refusal. See [refusals](/docs/collaboration/websocket#refusals). |
| `sync` | `EdytorSync` | | | Advanced: a custom provider factory such as `createIndexeddbSync(name)` or `createWebsocketSync(options)`. Attached in the browser when the view is editable; the view renders once the document is ready. See [Collaboration](/docs/collaboration). |
| `actor` | `DocumentActor` | anonymous | | The local author `{ id, name?, color? }` of the view's own document: undo lineage, per-block attribution and the presence profile peers see. Cannot be combined with `document` (set it in `createDocument`). |
| `readonly` | `boolean` | `false` | Yes | Render the document without editing. See [Readonly mode](/docs/editor/readonly). |
| `placeholder` | `string \| (view) => string \| null` | | | Text shown in an empty block. A function receives `{ type, data, focused, empty }` and returns the text or `null`. Falls back to the first plugin that declares a `placeholder`. |
| `blockHandles` | `boolean \| BlockHandlesOptions` | `true` | | Show a handle beside each block. `draggable: false` keeps the handle and its keyboard moves without pointer dragging. `onActivate({ block, anchor })` runs when the handle is clicked. `handle` is a snippet that replaces the handle's markup. See [Block handles](/docs/plugins/block-handles). |
| `blockDnd` | `boolean` | `true` | | **Deprecated.** `false` hides the handles. `blockHandles` wins when both are set. |
| `hotKeys` | `Partial<Record<HotKeyCombination, HotKey>>` | | | Key bindings (`'mod+s'`, `'alt+shift+arrowup'`, …) checked before the plugins' and the built-in ones. See [Hotkeys](/docs/customization/hotkeys#chord-syntax). |
| `onChange` | `(value: JSONBlock) => void` | | | Called after every committed change, local or remote, with the document as a root block `{ type: 'root', children }`. |
| `onSelectionChange` | `(selection: EdytorSelection) => void` | | | Called when the selection value changes. See [Selection](/docs/editor/selection). |
| `edytor` | `Edytor` | | Bindable | The editor instance. Use `bind:edytor`. See [The editor instance](/docs/concepts/editor-instance). |
| `class` | `string` | | Yes | Class on the editable root element. |
| `spellcheck` | `boolean` | `true` | Yes | The root's `spellcheck` attribute. |
| `autocorrect` | `'on' \| 'off'` | `'off'` | Yes | The root's `autocorrect` attribute. |
| `autocomplete` | `'on' \| 'off'` | `'off'` | Yes | The root's `autocomplete` attribute. |
| `autocapitalize` | `'off' \| 'none' \| 'on' \| 'sentences' \| 'words' \| 'characters'` | `'none'` | Yes | The root's `autocapitalize` attribute. |
| `inputmode` | `'none' \| 'text' \| 'decimal' \| 'numeric' \| 'tel' \| 'search' \| 'email' \| 'url'` | unset | Yes | Virtual keyboard hint. Omitted from the DOM when unset. |
| `enterkeyhint` | `'enter' \| 'done' \| 'go' \| 'next' \| 'previous' \| 'search' \| 'send'` | unset | Yes | Label of the virtual keyboard's action key. Omitted when unset. |
| `translate` | `'yes' \| 'no'` | `'no'` | Yes | The root's `translate` attribute. `'no'` keeps page translators from rewriting the editable text. |
| `doc` | `YDoc` | | | Advanced: build the view's own document around an existing engine document. Cannot be combined with `document`. |
| `awareness` | `Awareness` | | | Advanced: the presence instance for the view's own document. Cannot be combined with `document`. |

`value` is the initial content, not a binding: `bind:value` fails type-checking. Read changes with `onChange` or `edytor.value`.

### `document`, `room`/`server`/`sync` and `value` together

- **Only `value`**: the view creates its own document, seeds it, and destroys it on unmount.
- **`room`, `server` or `sync`**: the view creates its own document and attaches the provider (`sync` overrides `room`/`server`). Stored or remote content wins; `value` seeds the document only when the provider has nothing, after it answered or timed out. Never pass a changing snapshot (`onChange` output) as `value` beside a room: a late seed of it can replace the room's edits. Pass a fixed template or nothing; see [deterministic seeds](/docs/collaboration/documents#deterministic-seeds).
- **`document`**: the view renders that document once it is ready. If the document is still waiting for content and no provider is attached to it, the editable view seeds it with `value` when it mounts, unless a server refused it (`document.syncRefusal`): then an empty document stays waiting, and one that already holds content is shown. Passing `document` together with `doc`, `awareness` or `actor` throws.

A readonly view never connects: it shows `value`, or the `document` you pass.

### Block handles

Handles are on unless you turn them off: a `+` that adds a block and a drag grip. List [`blockMenuPlugin`](/docs/plugins/block-menu) to open Notion's block menu on a grip click, or pass `onActivate` to open your own:

```svelte
<script lang="ts">
	import { Edytor, type BlockHandleActivation } from 'edytor';

	let menu = $state<{ blockId: string; anchor: HTMLElement } | null>(null);
	const onActivate = ({ block, anchor }: BlockHandleActivation) => {
		menu = { blockId: block.id, anchor };
	};
</script>

<Edytor blockHandles={{ onActivate }} />
```

A focused handle moves its block with <kbd>Alt</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd> and nests or outdents it with <kbd>Alt</kbd>+<kbd>→</kbd>/<kbd>←</kbd>. Set the drop indicator color with the `--edytor-drop-indicator-color` custom property. To draw your own `+` and grip, pass a `handle` snippet: `blockHandles={{ handle }}` (see [Menus and handles](/docs/customization/menus#block-handle)).

## Snippets

Pass a snippet named `<type>Block`, `<type>Mark` or `<type>InlineBlock` to replace how one kind renders. The override replaces only the snippet: the kind's element, `void` and `island` flags, hooks and clipboard forms stay as the plugin defined them.

```svelte
<Edytor>
	{#snippet quoteBlock({ block, content, children })}
		<span class="quote-mark" contenteditable="false">“</span>
		{@render content()}
		{#if children}<div class="quote-children">{@render children()}</div>{/if}
	{/snippet}

	{#snippet boldMark({ content })}
		<span class="font-semibold">{@render content()}</span>
	{/snippet}
</Edytor>
```

| Snippet name | Receives |
| --- | --- |
| `<type>Block` | `{ block, content, children }`. `block` is a reactive view (`id`, `type`, `data`, `selected`, `focused`, `handle`); render `content()` for the block's text and `children()` (or `null` when it has none) for nested blocks. |
| `<type>Mark` | `{ content, mark, text }`. `mark` is the mark's value. The snippet renders inside a `<span data-edytor-mark>` instead of the mark's tag. |
| `<type>InlineBlock` | `{ block }`, a reactive view with `id`, `type`, `data`, `selected` and `handle`. |

The core renders the block element itself (the kind's `element`, a `div` by default), so a block snippet renders only the markup inside it. Mark non-editable chrome inside a block with `contenteditable="false"`, or with the `use:block.void` action.

For a type that is not a valid identifier, such as `todo-item`, spread the snippet under its name: `<Edytor {...{ 'todo-itemBlock': todo }} />`.

Content placed inside `<Edytor>` other than these snippets is ignored. To define a new kind rather than restyle one, write a plugin: see [Custom blocks](/docs/customization/blocks). To replace the slash menu, toolbar, block menu or block handle markup, pass snippets to their plugins: see [Menus and handles](/docs/customization/menus).
