Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Overview
    • Why This Fork
    • Examples
    • Installing Plite
    • Adding Event Handlers
    • Defining Custom Elements
    • Applying Custom Formatting
    • Executing Commands
    • Saving to a Database
    • Canonical Change Substrate
    • Improving Performance
    • Interfaces
    • Nodes
    • Locations
    • Transforms
    • Document Changes
    • Commands
    • Editor
    • Plugins
    • Rendering
    • Serializing
    • Normalizing
    • Using TypeScript
    • Roots
    • Document State
    • Editing Behavior
    • Selection And DOM
    • Clipboard And Paste
    • Decorations, annotations, and widgets
    • Schema
    • Anchor API
    • Location API
    • Path API
    • PointEntry API
    • Point API
    • Range API
    • Selection API
    • Location Types APIs
    • Span API
    • Editor
    • Element API
    • NodeEntry API
    • Node API
    • Node Types APIs
    • Text API
    • Debug Value Scrubbing
    • Transforms API
    • Plite DOM
    • History Editor API
    • History Plugin Setup
    • History
    • Plite History
    • Plite Hyperscript
    • Plite PaginationExperimental
    • Annotations
    • DOM Coverage Boundaries
    • Editable Component
    • External Text Views
    • Plite React Event Handling
    • Virtualized Rendering
    • Plite React Hooks
    • React Editor Setup
    • React Editor
    • Plite React
    • Plite Component
    • Plite Yjs
    • Plite
    • Migration
    • Contributing
    • FAQ
    • Resources

Why This Fork

PreviousNext

Why Plate maintains a Plite, what it optimizes for, and where the proof boundary is.

Plite is an agent-first editor substrate, not an official upstream Slate release. It keeps a plain JSON document model and rebuilds the runtime around canonical changes, explicit reads and writes, typed transactions, multi-root documents, state fields, and browser proof. This page explains the bet, the runtime shape, the package split, and the current proof limits.

On this page

  • The Main Fork
  • Why Not Stay Closer To Upstream Slate?
  • Who It Is For
  • What Stays From Slate
  • Runtime Shape
  • Document Shape
  • Plugin Model
  • React Runtime
OverviewInstalling Plite

On This Page

On this pageThe main forkWhy not stay closer to upstream Slate?Who it is forWhat stays from SlateRuntime shapeDocument shapePlugin modelReact runtimeOverlay modelBrowser proofPackage ownershipCurrent boundariesComplete change mapRead next
Build your editor
Production-ready AI template and reusable components.
Get all-access
  • Overlay Model
  • Browser Proof
  • Package Ownership
  • Current Boundaries
  • Complete Change Map
  • Read Next
  • The main fork

    The first fork is the maintenance model.

    Contenteditable bugs do not end. Browsers shift, selection breaks, paste paths fork, IME has edge cases, and large documents expose every hidden shortcut. A runtime that depends on occasional human cleanup will rot.

    This fork is built for the opposite shape:

    • agents run the boring loops;
    • humans steer taste, API boundaries, and product calls;
    • every serious behavior claim needs executable proof;
    • every regression should become a test, harness repair, or workflow repair;
    • APIs can be hard-cut when the old shape makes the system harder to verify.

    That is the real fork. The code matters, but the maintenance philosophy matters more.

    Why not stay closer to upstream Slate?

    The conservative design would keep upstream Slate core intact, rewrite plite-react, and push the hard work into the renderer. That would keep the public runtime closer to Slate 0.x while trying to unlock extra roots, document meta, overlays, and React performance from React alone.

    That path breaks down because the renderer cannot safely own the facts that make a rich-text editor correct. React can render a tree, but it cannot be the source of truth for write boundaries, selection authority, canonical changes, dirty regions, stable node identity, history grouping, collaboration metadata, and DOM repair.

    If plite-react tried to own those anyway, it would become a hidden second runtime beside Plite. The adapter would have to translate between mutable editor fields, React subscriptions, browser selection, history, collaboration, and test proof. That is the black box this fork avoids.

    ExampleWhy plite-react alone is the wrong owner
    Shift+Down in a huge documentThe runtime needs one selection target, one DOM-selection import/export path, and one dirty-render plan. A React component can patch symptoms, but it cannot make the model selection, native selection, and rendered corridor agree by itself.
    Cmd+A then DeleteThe editor needs one transaction that owns model deletion, selection collapse, history grouping, canonical change output, DOM repair, and follow-up typing. Splitting that across core mutation plus React cleanup makes the proof lie.
    Extra editable regionsHeaders, footers, sidebars, and body content need to serialize as one document value. If roots live only in React, normalization, history, collaboration, and persistence all get side channels.
    Document metadataValues like document.title must travel with the document model. Keeping them as app-only React state makes save/load, undo policy, collaboration patches, and tests disagree.
    Overlays and commentsDecorations and annotations use the same stable node identities and change facts, but keep separate owners. Treating them as React leaf hacks makes durable comments and transient search hits look the same.
    Browser proofA model-only assertion can pass while the caret is wrong. A DOM-only assertion can pass while Plite state is wrong. The proof harness needs one commit that model, DOM, native selection, focus, replay, and follow-up typing can all inspect.

    The fork keeps the useful core model: JSON nodes, paths, points, ranges, and app-owned schema. It makes DocumentChange the only mutation, mapping, history, and replay truth. The runtime uses editor.read(...), editor.update(...), transaction groups, commits, and state fields so React can subscribe through lazy commit.changed queries instead of guessing from broad tree changes.

    Lexical and ProseMirror are the closest comparisons. Lexical also separates reads from updates and reconciles dirty nodes. ProseMirror also routes every edit through a transaction and imports and exports DOM selection. The document layer stays Slate-shaped, which is the point: keep the flexible JSON model, replace the runtime shape that made React and browser proof fight the core.

    Who it is for

    Use this fork when you want the editor runtime maintained like infrastructure.

    Use Plite ifUse upstream Slate if
    You want agent-run regression loops, browser proof, API audits, and hard cuts.You want the familiar Slate surface with the smallest social and technical surprise.
    You own the editor integration and can move one surface at a time.You need a drop-in withHistory(withReact(createEditor())) upgrade path.
    You need multiple editable regions, document metadata, overlays, large-document proof, or Yjs adapter proof.You only need a small hand-maintained editor and do not want the new runtime contract.
    You accept docs that are dense, exact, and proof-oriented.You want docs optimized first for a gentle tutorial read.

    This is not a nicer wrapper around Slate 0.x. It is a stricter runtime for teams that prefer proof and maintainability over compatibility comfort.

    What stays from Slate

    The fork keeps the parts of Slate that make the model worth preserving:

    • documents are nested JSON nodes;
    • selections use paths, points, and ranges;
    • edits produce one root-aware DocumentChange;
    • schema is application-owned;
    • React renders the editor instead of owning the document model.

    Those choices are still good. The fork changes the runtime around them.

    Runtime shape

    The public authoring model has two boundaries:

    const value = editor.read.value();
     
    editor.update((tx) => {
      tx.text.insert("Hello");
    });
    const value = editor.read.value();
     
    editor.update((tx) => {
      tx.text.insert("Hello");
    });

    Reads go through editor.read(...). Writes go through editor.update(...). Inside an update, transaction groups own text, nodes, fragments, selection, marks, roots, history, canonical changes, effects, and plugin namespaces.

    SurfaceJob
    editor.read(...)read committed runtime state without mutating it
    editor.update(...)run one write transaction and commit the result
    tx.textinsert and delete text
    tx.nodesinsert, set, remove, split, merge, wrap, unwrap, and move nodes
    tx.fragmentreplace or delete closed fragments
    tx.selectionset, clear, collapse, and move model selection
    tx.marksread, set, toggle, add, and remove active marks
    tx.rootscreate, replace, and delete extra roots
    editor.api.historyreplay complete undo and redo batches when plitejs/history is installed
    tx.changesapply canonical root-aware document changes
    tx.effectsemit typed state and integration effects

    Each committed edit owns one DocumentChange. Commands, tags, effects, and annotations describe why an update ran without becoming a second document truth.

    Document shape

    The short value is still an array of blocks:

    const initialValue = [{ type: "paragraph", children: [{ text: "Body" }] }];
    const initialValue = [{ type: "paragraph", children: [{ text: "Body" }] }];

    Use a full document value when one editor owns extra editable regions or serialized metadata:

    const initialValue = {
      children: [{ type: "paragraph", children: [{ text: "Body" }] }],
      roots: {
        header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
        footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
      },
      meta: {
        "document.title": { value: "Launch brief", version: 1 },
      },
    };
    const initialValue = {
      children: [{ type: "paragraph", children: [{ text: "Body" }] }],
      roots: {
        header: [{ type: "paragraph", children: [{ text: "Draft" }] }],
        footer: [{ type: "paragraph", children: [{ text: "Internal" }] }],
      },
      meta: {
        "document.title": { value: "Launch brief", version: 1 },
      },
    };

    children is the primary editable body. roots stores extra editable regions. meta stores versioned envelopes produced by registered state-field codecs.

    Omit root for the primary editable. Root keys are for extra document regions.

    Plugin model

    Plugins add named read and update groups. They do not replace the canonical change builder or patch random methods onto the editor object.

    import { definePlugin } from "plitejs";
     
    const TodoPlugin = definePlugin("todo", {
      update: ({ tx }) => ({
        toggle() {
          tx.nodes.set({ checked: true, type: "todo" });
        },
      }),
    });
    import { definePlugin } from "plitejs";
     
    const TodoPlugin = definePlugin("todo", {
      update: ({ tx }) => ({
        toggle() {
          tx.nodes.set({ checked: true, type: "todo" });
        },
      }),
    });

    This keeps method-style ergonomics while making ownership visible. App commands live in the runtime instead of in ad hoc editor mutation helpers.

    React runtime

    React editors start with useEditor, <EditorRoot>, and <Editable>.

    import { Editable, EditorRoot, useEditor } from "plitejs/react";
     
    const Editor = () => {
      const editor = useEditor({
        initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
      });
     
      return (
        <EditorRoot editor={editor}>
          <Editable placeholder="Start typing..." />
        </EditorRoot>
      );
    };
    import { Editable, EditorRoot, useEditor } from "plitejs/react";
     
    const Editor = () => {
      const editor = useEditor({
        initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
      });
     
      return (
        <EditorRoot editor={editor}>
          <Editable placeholder="Start typing..." />
        </EditorRoot>
      );
    };

    useEditor installs the React, DOM, and clipboard path. Install history() explicitly for undo/redo. Use createEditor when the editor must be created outside component ownership.

    React rendering is selector-first. App chrome should subscribe to the editor facts it needs, not to the whole editor tree.

    AreaRuntime owner
    Editable renderingplitejs/react
    DOM conversion and coverageplitejs/dom
    Historyplitejs/history
    Extra rootsplitejs plus root-aware React hooks
    Large-document proof lanesplitejs/react, plitejs/dom, @platejs/test, and benchmarks

    Overlay model

    The fork treats overlays as separate lanes.

    LanePurpose
    Decorationstransient ranges such as search hits, syntax, diagnostics, and active matches
    Annotationsdurable, id-bearing anchors such as comments, review threads, and remote markers

    Decorations and annotations share stable identities and change facts but keep separate owners and storage.

    Pass transient paint sources to <EditorRoot decorations={...}>. Each source reads attribute-only ranges and may observe its own store. Keep durable anchors in an annotation store and pass it explicitly to useAnnotation(store, id) or useAnnotations(store).

    Browser proof

    Model tests are not enough for editor behavior. DOM checks are not enough either.

    For cursor, selection, paste, undo, hidden DOM, large documents, and collaboration-adjacent behavior, the fork expects proof across the layers that can actually fail.

    LayerWhy it matters
    Modelproves Plite state is correct
    DOMproves rendered text and elements match the model
    Native selectionproves the browser caret and selection agree where observable
    Focuscatches editor activation and chrome bugs
    Commit metadataproves the runtime knows what changed
    Replaycatches proof that only passes once
    Follow-up typingproves the editor still works after the asserted state

    That is why @platejs/test exists. It is not the product editing API. It is the proof harness used to keep the editor honest.

    Package ownership

    The packages are split by responsibility.

    PackageOwns
    plitejseditor runtime, document model, canonical changes, transactions, state fields, schema, pure helper APIs
    plitejs/domDOM bridge, clipboard helpers, selection conversion, hotkeys, DOM coverage helpers
    plitejs/reactReact editor setup, <EditorRoot>, <Editable>, render primitives, hooks, overlays, annotations, DOM strategies
    plitejs/historyhistory plugin and undo/redo runtime
    plitejs/hyperscriptJSX fixtures for tests
    plitejs/yjsYjs collaboration adapter, awareness bridge, remote cursor hooks
    plitejs/paginationimmutable page measurement and the view-owned PagedEditable surface
    @platejs/testNode-safe fixtures plus React, DOM, Playwright, and proof helpers behind explicit subpaths

    Apps should import public package exports. /internal subpaths are for sibling packages in this repo.

    Current boundaries

    This fork is intentionally narrow about what it claims.

    AreaClaim
    Desktop browser editingIn scope when covered by browser proof.
    Release statusPre-1.0. No production-readiness claim; public APIs can still be hard-cut.
    Raw mobile device behaviorNot claimed without real device proof artifacts.
    Pagination and page layoutLive page measurement and editing are supported; identical cross-platform export geometry needs a controlled measurement environment.
    Collaborationplitejs/yjs owns the adapter. Apps own provider packages, auth, persistence, room naming, and server policy.
    Upstream identityThis is Plite, not an official upstream Slate release.

    The line is simple: if the proof lane does not own it, the docs should not pretend it is done.

    Complete change map

    This table is intentionally exhaustive. It gives humans and agents one place to route deeper reading.

    AreaWhat changed
    Core lifecycleeditor.read(...), editor.update(...), state/transaction runtime namespaces, commits, and owner-local read/update groups
    Core data modelimmutable JSON nodes, snapshots, root-aware DocumentChange, node keys, anchors, and extensible selections
    Public helper APIsElementApi, LocationApi, NodeApi, PathApi, PointApi, RangeApi, SelectionApi, SpanApi, TextApi, and DocumentChange
    PluginsdefinePlugin, schema elements, state fields, effects, corrections, prefixless on listeners, and name-scoped API/read/update groups
    Document valuesprimary children, optional extra roots, optional persistent meta, optional keyed records
    Rootsrootless primary document, named extra roots, content roots, root-aware React hooks and chrome
    State fieldsversioned persistent/shared fields, local fields, history policy, effects, React state-field hooks
    React setupuseEditor, createEditor, <EditorRoot>, <Editable>, render primitives
    React hookseditor state hooks, element hooks, root hooks, state-field hooks, decoration and annotation hooks
    Browser editingDOM repair, native selection sync, beforeinput/input/paste/cut/drop authority, generated proof scenarios
    DOM bridgeDOM point/range conversion, clipboard bridge, hotkeys, DOM coverage boundaries, browser environment helpers
    Overlaysdecoration manager, annotation stores, source-scoped invalidation
    Large documentscomplete DOM rendering, explicit virtualized rendering, bounded editing proof, huge-document smoke lanes
    Historyhistory() plugin, state.history, editor.api.history, transaction grouping controls, semantic editor.update({ history }) policies, and history stack validation
    HyperscriptJSX fixture factory, custom fixture tags, low-level fixture creators
    LayoutmeasurePages, PagedEditable, explicit node fragmentation, page mount plans, and current-element fragment hooks
    Browser proof@platejs/test/proof, @platejs/test/browser, @platejs/test/playwright, feature contracts, screenshots, traces, replay, and explicit non-replayable imperative runs
    Docsmigration guide, walkthroughs, API reference, roots/meta docs, React setup docs, DOM coverage docs, layout docs
    Examplesplaintext, richtext, checklists, markdown, inlines, mentions, images, embeds, tables, paste HTML, placeholder, read-only, iframe/shadow DOM, styling, document meta, hidden content, huge document, multi-root document, synced blocks, comment mode, search/code highlighting, Yjs examples
    Hard cutsrootless primary APIs, explicit reads/writes, transaction groups, no public compatibility aliases, no primary mutable editor-field authoring story

    Read next

    • Installing Plite
    • Migration
    • React Editor Setup
    • Roots
    • Document Meta
    • Plite Pagination

    The fork's promise is narrow: Plite's model, rebuilt for agent-run maintenance and proof-heavy editor behavior.