Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
1.0. Edytor
1.0.0is on npm asedytor: its API follows semantic versioning, and every change is listed in the changelog. It has no production track record yet: read the limitations before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
- Notion-style editing out of the box.
<Edytor />alone is a rich text editor with images and block handles: headings, toggle headings, lists, to-dos, toggles, callouts, quotes, dividers, ten marks and Notion's hotkeys. Add the Notion theme, a block menu (block colours; drag one block, or every block the selection covers), a slash menu, a selection toolbar, markdown shortcuts (createMarkdownShortcutsPlugin()) and code blocks. - Pages and a table of contents, opt-in.
createPagePluginlinks a block to another document (a page per room, opened and named by your app), andcreateTocPlugin()lists the headings live (Page, Table of contents). - Columns, opt-in. List
createColumnsPlugin()for Notion's multi-column layouts: drag a block to the edge of another to put them side by side, or pick "2 columns" to "5 columns"; drag the gap to resize. Layouts converge across collaborators and the room (Columns). - Tables, opt-in. List
createTablePlugin()for Notion's simple table: cells of text, Tab between them, rows and columns added, moved and deleted from the table's edges and grips, resizable columns, header row and column, HTML tables pasted and copied. Concurrent row and column edits converge on every replica (Table). - Mentions, page links and your own triggers.
createMentionPlugin(@people) andcreatePageLinkPlugin([[pages) take your app's search; a plugin'sinputRules(:smile:→ 😄) andtriggers(a menu on a typed character) declare the rest (Input rules and triggers). - Comments, opt-in.
createCommentsPluginadds Notion's comment threads: select text, press Comment, and the thread shows in a sidebar beside it with replies, Resolve and Re-open. The room keeps the threads beside the document and sends each change live; the anchor follows the text through edits and is not copied (Comments). - Your markup. Blocks, marks and inline atoms render through your Svelte snippets; the slash menu, toolbar, block menu and handles take a snippet and keep their behavior.
- Your language. Every word the package shows or says, slash keywords included, comes from one labels dictionary the view takes (
<Edytor labels>); your chrome's words are your markup's; English by default (Localization). - Synced properties.
block.data,atom.dataandedytor.dataread like plain objects, andedytor.patchData(id, [{ path, value }])writes them; each property and each array item syncs on its own, so concurrent edits of different properties, or of different items of one array, merge. - AI suggestions.
edytor.suggestions.add(position, content)proposes text, paragraphs, lists, to-dos or images after, before or inside a block, at the end of its text, or in place of the selection; it streams in, shows only on your screen, and becomes one undo step when accepted (Suggestions). - Plugins that can veto anything. Every command is prepared before it writes; plugins see the command and each planned step and can refuse or replace it.
- Real-time collaboration. One
EdytorDocumentshared by any number of views, or none (headless). Presence cursors, identity-preserving moves, splits and merges, and undo that only takes back your own edits. - Offline first. A local IndexedDB copy and cross-tab sync; offline edits survive reloads and reach the server on reconnect.
- A Cloudflare Durable Object room.
edytor/cloudflarestores in SQLite, acknowledges after the write, binds identity to the socket and hibernates. - Headless and Worker-safe.
edytor/crdt/edytorruns in Node and Workers without Svelte.
The pre-release is on npm under the next tag. Name the tag: a bare edytor is the old, incompatible 0.0.11.
pnpm add edytorsvelte@^5 is a peer dependency. Do not install yjs: the v14 engine is vendored (edytor/crdt). See installation and platform support (Baseline 2024: Chrome and Edge 120, Firefox 125, Safari 17.4 and later; real phones are not tested yet).
The package draws no menu, toolbar or block handle: each is a snippet you pass its plugin. Without them the editor works but shows none. The demo's ready-made, Notion-style ones are one copy away: copy src/routes/chrome into your app, make its imports name the package, and pass its plugins (quick start):
npx degit beynar/edytor/src/routes/chrome src/lib/chrome
sed -i.bak 's#\$lib/index\.js#edytor#' src/lib/chrome/*.ts src/lib/chrome/*.svelte && rm src/lib/chrome/*.bakTake the folder of the version you install, by its tag (beynar/edytor/src/routes/chrome#v1.0.0): without one you get master's, which follows the next release.
<script lang="ts">
import { Edytor } from 'edytor';
import 'edytor/themes/notion.css';
import {
slashMenuPlugin,
toolbarPlugin,
blockHandlesPlugin,
blockMenuPlugin,
codePlugin
} from '$lib/chrome/plugins';
</script>
<div class="edytor-notion">
<Edytor
plugins={[slashMenuPlugin, toolbarPlugin, blockHandlesPlugin, blockMenuPlugin, codePlugin]}
/>
</div>Or draw a menu yourself: your snippet draws it, the plugin gives it its keys and ARIA.
<script lang="ts">
import {
Edytor,
createCodePlugin,
createSlashMenuPlugin,
createMarkdownShortcutsPlugin,
richTextPlaceholder,
type JSONDoc,
type SlashMenuController
} from 'edytor';
import 'edytor/themes/notion.css';
const value: JSONDoc = {
children: [{ type: 'paragraph', content: [{ text: 'Hello, World!' }] }]
};
const plugins = [
// Notion's hints in empty blocks.
() => ({ placeholder: richTextPlaceholder }),
createCodePlugin(),
createMarkdownShortcutsPlugin(),
createSlashMenuPlugin({ menu })
];
</script>
<!-- Your snippet draws the menu; the plugin's attachments give it its keys and ARIA. -->
{#snippet menu(slash: SlashMenuController)}
<ul {@attach slash.popup}>
{#each slash.commands as command, index (command.id)}
<li
data-selected={slash.index === index}
{@attach slash.option(index)}
onclick={() => void slash.run(command)}
>
{command.label}
</li>
{/each}
</ul>
{/snippet}
<div class="edytor-notion">
<Edytor {value} {plugins} onChange={(root) => console.log(root)} />
</div>An editable editor is client-only: in SvelteKit, mount it once the page has hydrated (quick start). <Edytor> adds the rich text, image, arrow-move and suggestions plugins after yours. Every menu, toolbar, handle and panel is drawn by a snippet, yours or the demo's (menus and handles). To collaborate, name a room and a server running the edytor/cloudflare room:
<Edytor
server="wss://example.com/rooms"
room={documentId}
params={{ token }}
actor={{ id: userId }}
/>The documentation site is the single source for the API and behavior; this README only introduces the package.
- Getting started: installation, quick start, SvelteKit, entry points and bundle size
- Concepts: the document model, blocks, void and island roles
- Editor: the component, commands, selection, history, clipboard, readonly
- Plugins and customization: bundled plugins, custom blocks and marks, hotkeys and editing behavior
- Collaboration and server: documents, providers, presence, the Durable Object room and its protocol
- Reference: the document API, troubleshooting, the changelog (with migration from 0.0.11), limitations, platform support
CONTRIBUTING.md lists the test lanes, what CI runs, and the pull request checklist; AGENTS.md describes the architecture (one owner per fact) and where to fix what, and docs/agents/ holds the rules of each area. The docs live in site/content/docs; a change to public behavior updates them in the same commit.
pnpm install
pnpm check && pnpm lint
pnpm exec vitest --run # unit and model fixtures (JSX fixture DSL in src/tests/jsx)
pnpm test:dom # editors mounted in jsdom