From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
UNPUBLISHED
  • Overview
  • Why This Fork
  • Examples
Walkthroughs
  • Installing Plite
  • Adding Event Handlers
  • Defining Custom Elements
  • Applying Custom Formatting
  • Executing Commands
  • Saving to a Database
  • Canonical Change Substrate
  • Improving Performance
Concepts
  • Interfaces
  • Nodes
  • Locations
  • Transforms
  • Document Changes
  • Commands
  • Editor
  • Extensions
  • Rendering
  • Serializing
  • Normalizing
  • Using TypeScript
  • Roots
  • Document State
  • Editing Behavior
  • Selection And DOM
  • Clipboard And Paste
  • Projection And Overlays
  • Schema
API
  • 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
Libraries
  • Plite DOM
  • History Editor API
  • History Extension Setup
  • History
  • Plite History
  • Plite Hyperscript
  • Plite Layout
  • Annotations
  • DOM Coverage Boundaries
  • Editable Component
  • Plite React Event Handling
  • Virtualized Rendering
  • Plite React Hooks
  • React Editor Setup
  • React Editor
  • Plite React
  • Plite Component
  • Plite Yjs
  • Plite
General
  • Migration
  • Contributing
  • Docs Proof Map
  • FAQ
  • Resources

Extensions

PreviousNext

Package reusable Plite schema, reads, updates, APIs, and lifecycle behavior.

Extensions package reusable Plite behavior that should be installed, ordered, and removed as one unit. Plate builds its product-level plugin conventions on this smaller runtime primitive.

On This Page

  • Smallest Extension
  • Read And Update Helpers
  • Extension APIs
  • Extension Order
  • Typed Extension Points
  • Activation And Reconfiguration
  • Schema Contributions
  • Read Middleware
EditorRendering

On This Page

On This PageSmallest ExtensionRead And Update HelpersExtension APIsExtension OrderTyped Extension PointsActivation And ReconfigurationSchema ContributionsRead MiddlewareCommands And CorrectionsClipboard And Fragment PolicySlot ReferenceType Inference
Build your editor
Production-ready AI template and reusable components.
Get all-access
Commands And Corrections
  • Clipboard And Fragment Policy
  • Slot Reference
  • Type Inference
  • Smallest Extension

    defineExtension(...) preserves the literal extension name and lets createEditor(...) infer every installed capability. A complete schema is also an extension, created with defineEditorSchema(...).

    import {
      createEditor,
      defineExtension,
      defineEditorSchema,
      schema,
    } from "@platejs/plite";
     
    const ArticleSchema = defineEditorSchema("schema:article", {
      elements: {
        paragraph: {
          content: schema.content.text({ default: "text", min: 1 }),
        },
      },
      id: "article",
      root: schema.content.type("paragraph", {
        default: { type: "paragraph" },
        min: 1,
      }),
      unknown: "reject",
      version: 1,
    });
     
    const ArticleExtension = defineExtension("article", {});
     
    const editor = createEditor({
      extensions: [ArticleSchema, ArticleExtension] as const,
      initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
    });
    import {
      createEditor,
      defineExtension,
      defineEditorSchema,
      schema,
    } from "@platejs/plite";
     
    const ArticleSchema = defineEditorSchema("schema:article", {
      elements: {
        paragraph: {
          content: schema.content.text({ default: "text", min: 1 }),
        },
      },
      id: "article",
      root: schema.content.type("paragraph", {
        default: { type: "paragraph" },
        min: 1,
      }),
      unknown: "reject",
      version: 1,
    
    
    
    
    
    
    
    

    Use feature extensions for reusable behavior and partial schema contributions. Use one complete schema to close the document vocabulary. See Schema for the declaration and compiler model.

    Read And Update Helpers

    Use read for owner-local queries and update for owner-local model writes. The extension name becomes the namespace on editor.read and editor.update.

    import { createEditor, defineExtension } from "@platejs/plite";
     
    const LinksExtension = defineExtension("links", {
      read: ({ state }) => ({
        hasSelection() {
          return state.selection() !== null;
        },
      }),
      update: ({ tx }) => ({
        setHref(href: string) {
          tx.nodes.set({ url: href });
        },
      }),
    });
     
    const editor = createEditor({
      extensions: [LinksExtension],
    });
     
    const canEditLink = editor.read((state) => state.links.hasSelection());
     
    editor.update.links.setHref("https://example.com");
     
    editor.update((tx) => {
      tx.links.setHref("https://example.com");
    });
    import { createEditor, defineExtension } from "@platejs/plite";
     
    const LinksExtension = defineExtension("links", {
      read: ({ state }) => ({
        hasSelection() {
          return state.selection() !== null;
        },
      }),
      update: ({ tx }) => ({
        setHref(href: string) {
          tx.nodes.set({ url: href });
        },
      }),
    });
     
    const editor = createEditor({
      extensions: [LinksExtension],
    
    
    
    
    
    
    
    
    

    Plite constructs each read namespace once for an installed extension configuration. Return methods or nested method records. Read live document state inside those methods; do not return document-derived data properties or perform reads while constructing the namespace. Put stable constants and host services in api.

    Direct calls such as editor.read.links.hasSelection() execute inside the same coherent read boundary as editor.read((state) => ...).

    Reads cannot write by accident. Updates can still read transaction-local state after earlier writes in the same transaction. Wrap a method with txOnly(...) when it requires an active transaction; Plite then omits it from the direct editor.update group.

    An update helper should perform Plite model writes. Keep UI, layout, network, and other host services in api.

    Extension APIs

    api is the single host-service channel. Declare it as a factory even when it needs no context. Its single context object exposes editor, root, and getContributions. Plite publishes the returned object through the extension name and through the exact descriptor portal.

    const LinksExtension = defineExtension("links", {
      api: () => ({
        normalizeHref(href: string) {
          return new URL(href).toString();
        },
      }),
    });
     
    const editor = createEditor({
      extensions: [LinksExtension],
    });
     
    editor.api.links.normalizeHref("https://example.com");
    editor.extension(LinksExtension).api.normalizeHref("https://example.com");
    const LinksExtension = defineExtension("links", {
      api: () => ({
        normalizeHref(href: string) {
          return new URL(href).toString();
        },
      }),
    });
     
    const editor = createEditor({
      extensions: [LinksExtension],
    });
     
    editor.api.links.normalizeHref("https://example.com");
    editor.extension(LinksExtension).api.normalizeHref("https://example.com");

    Use editor.api.links when application code knows the installed editor shape. Use editor.extension(LinksExtension).api when generic package code owns the descriptor. Both paths reference the same immutable API object.

    Do not use api as a document command bus. Document mutations belong in update; host services such as DOM focus, measurements, history batching, or framework bridges belong in api.

    Extension Order

    Use descriptor-valued dependencies when one extension requires another. Use conflicts when two descriptors cannot be installed together.

    const LinksExtension = defineExtension("links", {
      api: () => ({
        normalizeHref(href: string) {
          return new URL(href).toString();
        },
      }),
    });
     
    const MentionsExtension = defineExtension("mentions", {
      dependencies: [LinksExtension],
      update: ({ tx }) => ({
        insert(character: string) {
          tx.nodes.insert({
            type: "mention",
            character,
            children: [{ text: "" }],
          });
        },
      }),
    });
    const LinksExtension = defineExtension("links", {
      api: () => ({
        normalizeHref(href: string) {
          return new URL(href).toString();
        },
      }),
    });
     
    const MentionsExtension = defineExtension("mentions", {
      dependencies: [LinksExtension],
      update: ({ tx }) => ({
        insert(character: string) {
          tx.nodes.insert({
            type: "mention",
            character,
            children: [{ text: 
    
    
    
    

    Plite installs LinksExtension transitively before MentionsExtension. Shared dependencies activate once and remain installed until their last direct or transitive owner is removed. Configuration fails for dependency cycles, duplicate active names, or installed conflicting descriptors.

    The public dependency reference is deliberately shallow: literal name and optional enabled, with no generic capability graph. Plite keeps its finite name-keyed capability/provider inference under @platejs/plite/internal instead of recursively publishing dependency ancestry. Static portal types prove that one installed name has the same capability as the descriptor; runtime access separately requires the exact installed descriptor identity.

    Typed Extension Points

    Use defineExtensionPoint(...) when another package owns an ordered typed contribution channel outside the headless model.

    import {
      defineExtension,
      defineExtensionPoint,
    } from "@platejs/plite";
     
    type MeasurementSource = {
      measure(): number;
    };
     
    const measurements =
      defineExtensionPoint<MeasurementSource>("app:measurement-source");
     
    const MeasuredExtension = defineExtension("measured", {
      contributions: [
        measurements.of({
          measure: () => performance.now(),
        }),
      ],
    });
    import {
      defineExtension,
      defineExtensionPoint,
    } from "@platejs/plite";
     
    type MeasurementSource = {
      measure(): number;
    };
     
    const measurements =
      defineExtensionPoint<MeasurementSource>("app:measurement-source");
     
    const MeasuredExtension = defineExtension("measured", {
      contributions: [
        measurements.of({
          measure: () => performance.now(),
        }),
      ],
    });

    The extension point owns identity and type. Validators collect its ordered values with context.getContributions(measurements), so arbitrary strings do not become a hidden dependency API.

    Activation And Reconfiguration

    Keep schema, read, update, command, and API declarations on the extension. Use activate(editor, context) only for synchronous resource ownership. Register cleanup before activation returns, and defer external work that must observe the published candidate to context.afterPublish(...).

    const TableNavigationExtension = defineExtension("tableNavigation", {
      update: ({ tx }) => ({
        insertRow() {
          tx.nodes.insert({
            type: "table-row",
            children: [{ type: "table-cell", children: [{ text: "" }] }],
          });
        },
      }),
      activate(_editor, context) {
        const controller = createTableNavigationController();
     
        context.onCleanup(({ reason }) => controller.dispose(reason));
        context.afterPublish(() => controller.start({ signal: context.signal }));
      },
      validate(context) {
        validateTableContributions(context.getContributions(tableContributions));
      },
    });
    const TableNavigationExtension = defineExtension("tableNavigation", {
      update: ({ tx }) => ({
        insertRow() {
          tx.nodes.insert({
            type: "table-row",
            children: [{ type: "table-cell", children: [{ text: "" }] }],
          });
        },
      }),
      activate(_editor, context) {
        const controller = createTableNavigationController();
     
        context.onCleanup(({ reason }) => controller.dispose(reason));
        context.afterPublish(() => controller.start
    
    
    
    
    

    Plite compiles and validates a detached candidate before publication. API factories see the same declarative schema and descriptor-resolved dependency APIs. Factory results become visible together in the final candidate passed to validate(context). Activation runs only after every validator succeeds.

    Lifecycle callbacks are synchronous and cannot publish editor writes. Cleanup receives remove, replace, or rollback as its reason; lifecycle failures report through the editor error sink.

    Use a named extension slot when installed behavior must change after editor creation. The replacement is staged in the surrounding transaction and becomes visible with the same commit as document and state changes.

    import { createEditor, defineExtensionSlot } from "@platejs/plite";
     
    const navigation = defineExtensionSlot("navigation");
    const editor = createEditor({
      extensions: [navigation.of(TableNavigationExtension)] as const,
    });
     
    editor.update((tx) => {
      tx.extensions.reconfigure(navigation, AlternativeNavigationExtension);
    });
    import { createEditor, defineExtensionSlot } from "@platejs/plite";
     
    const navigation = defineExtensionSlot("navigation");
    const editor = createEditor({
      extensions: [navigation.of(TableNavigationExtension)] as const,
    });
     
    editor.update((tx) => {
      tx.extensions.reconfigure(navigation, AlternativeNavigationExtension);
    });

    When a replacement schema rejects the current document, pass a migrate callback. Compilation, migration, and validation finish before one document-plus-extension commit; failure publishes nothing. See Atomic Reconfiguration.

    editor.install(...) is the host-level dynamic form. Prefer a named slot when the replacement must commit atomically with other model writes.

    Schema Contributions

    Feature extensions add document vocabulary through schema. Element-owned properties live with the element; cross-cut properties use a top-level property declaration with an explicit target.

    import { defineExtension, property, schema } from "@platejs/plite";
     
    const tableModel = {
      schema: {
        elements: {
          tableCell: {
            content: schema.content.group("block", {
              default: { type: "paragraph" },
              min: 1,
            }),
            isolating: true,
            keyboardSelectable: true,
            properties: {
              colSpan: property.number({ default: 1, omitDefault: true }),
              rowSpan: property.number({ default: 1, omitDefault: true }),
            },
          },
        },
      },
    } as const;
     
    const tableCell = schema.handle.element(tableModel, "tableCell");
    const colSpan = schema.handle.property(tableCell, "colSpan");
     
    const TablesExtension = defineExtension("tables", {
      ...tableModel,
      read: ({ state }) => ({
        selectedCellColSpan(element) {
          return state.schema.getElementProperty(element, colSpan);
        },
      }),
    });
    import { defineExtension, property, schema } from "@platejs/plite";
     
    const tableModel = {
      schema: {
        elements: {
          tableCell: {
            content: schema.content.group("block", {
              default: { type: "paragraph" },
              min: 1,
            }),
            isolating: true,
            keyboardSelectable: true,
            properties: {
              colSpan: property.number({ default: 1, omitDefault: true }),
              rowSpan: property.number({ default: 1, omitDefault: true }),
            },
          },
        },
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    A schema factory receives only the extension name. Capture immutable domain inputs in the factory that creates the descriptor.

    const createCommentsExtension = (prefix: string) =>
      defineExtension("comments", {
        schema: ({ name }) => ({
          properties: [
            schema.textProperty(
              schema.key.prefix(`${prefix}${name}_`),
              property.boolean({ default: false, omitDefault: true }),
              {
                split: "preserve",
                target: target.group("element"),
                typeChange: "preserve-if-allowed",
              }
            ),
          ],
        }),
      });
    const createCommentsExtension = (prefix: string) =>
      defineExtension("comments", {
        schema: ({ name }) => ({
          properties: [
            schema.textProperty(
              schema.key.prefix(`${prefix}${name}_`),
              property.boolean({ default: false, omitDefault: true }),
              {
                split: "preserve",
                target: target.group("element"),
                typeChange: "preserve-if-allowed",
              }
            ),
          ],
    
    

    Element declarations can describe inline, void, atom, isolating, keyboardSelectable, readOnly, selectable, and markableVoid behavior. See Selection And DOM and Roots for the associated selection and root laws.

    Read Middleware

    Use readMiddleware to wrap a declared core read. Register middleware against an editorReads descriptor; the descriptor infers the input, result, editor, and state types.

    import {
      defineExtension,
      editorReads,
    } from "@platejs/plite";
     
    const ClosedSectionsExtension = defineExtension("closedSections", {
      readMiddleware: ({ around }) => [
        around(
          editorReads.nodes.isSelectable,
          ({ input: { element }, next }) =>
            element.closed === true ? false : next()
        ),
      ],
    });
    import {
      defineExtension,
      editorReads,
    } from "@platejs/plite";
     
    const ClosedSectionsExtension = defineExtension("closedSections", {
      readMiddleware: ({ around }) => [
        around(
          editorReads.nodes.isSelectable,
          ({ input: { element }, next }) =>
            element.closed === true ? false : next()
        ),
      ],
    });

    Middleware runs in extension order. next() delegates once and accepts a replacement input when needed. The provided state is read-only and observes an active transaction draft.

    Use editorReads.slice.export for ordered export projection:

    const CleanExportExtension = defineExtension("cleanExport", {
      readMiddleware: ({ around }) => [
        around(editorReads.slice.export, ({ next }) => {
          const slice = next();
     
          return {
            ...slice,
            content: removeTransientProperties(slice.content),
          };
        }),
      ],
    });
    const CleanExportExtension = defineExtension("cleanExport", {
      readMiddleware: ({ around }) => [
        around(editorReads.slice.export, ({ next }) => {
          const slice = next();
     
          return {
            ...slice,
            content: removeTransientProperties(slice.content),
          };
        }),
      ],
    });

    Commands And Corrections

    Use defineCommand(...) when an action needs headless evaluation or extension policy. Its pure builder and extension handlers return false or a frozen TransactionSpec; commands registers typed interceptors without patching editor methods. See Commands for the complete flow.

    Use corrections for deterministic schema repairs triggered by canonical changed ranges. A correction writes through its provided transaction and must converge. Keyboard events and host UI remain React or DOM adapter concerns.

    Clipboard And Fragment Policy

    DOM clipboard ingress is a typed extension point owned by @platejs/plite-dom. Add clipboardHandler(...) to contributions. Its insertData callback receives the DataTransfer, next, and transaction. Mutate through that transaction and return true when the handler owns the payload. Return next() to keep Plite's slice and plain-text fallback in the same transaction. See Clipboard And Paste.

    Wrap editorReads.slice.export with readMiddleware to sanitize copied or dragged content. Call editor.read.slice.export() when content leaves the editor; editor.read.slice.get() returns the structural selection slice unchanged.

    Slot Reference

    Start with schema, read, and update. Reach for the other slots only when the behavior needs that runtime phase.

    SlotUse it for
    namestable extension identity
    enableddeclarative inclusion
    dependenciesdescriptors installed before this extension
    conflictsdescriptors that cannot be installed with this extension
    schemaelement, property, group, and root contributions
    readowner-local read helpers
    updateowner-local atomic and direct update helpers
    readMiddlewaretyped middleware over editorReads descriptors
    commandspure typed command interceptors
    correctionsdeterministic changed-range structural repairs
    stateFieldstyped persisted or runtime state descriptors
    effectTypestyped commit-effect descriptors and codecs
    facetProvidersderived state providers
    apihost services exposed through the extension namespace
    contributionstyped ordered values consumed through an extension point
    on.*commit, transactionChange, nodeChange, and textChange
    activatesynchronous resource ownership and cleanup registration
    validatedetached candidate validation

    Keep product conventions above these raw slots. Frameworks can build richer feature APIs on Plite's smaller extension substrate.

    Type Inference

    Let defineExtension(...) infer its one definition type from the author object. DefinitionOf<typeof Extension> recovers the compact public contract when generic library code needs it.

    import {
      type DefinitionOf,
      defineExtension,
    } from "@platejs/plite";
     
    const ImagesExtension = defineExtension("images", {
      schema: {
        elements: {
          image: {
            void: "block",
          },
        },
      },
      update: ({ tx }) => ({
        insert(url: string) {
          tx.nodes.insert({
            type: "image",
            url,
            children: [{ text: "" }],
          });
        },
      }),
    });
     
    type ImagesDefinition = DefinitionOf<typeof ImagesExtension>;
    import {
      type DefinitionOf,
      defineExtension,
    } from "@platejs/plite";
     
    const ImagesExtension = defineExtension("images", {
      schema: {
        elements: {
          image: {
            void: "block",
          },
        },
      },
      update: ({ tx }) => ({
        insert(url: string) {
          tx.nodes.insert({
            type: "image",
            url,
            children: [{ text: "" }],
          });
        },
    
    
    
    

    Do not annotate callback contexts or pass a separate editor generic. The definition factory, installed descriptor tuple, and descriptor portal preserve the inferred capabilities.

    });
    const ArticleExtension = defineExtension("article", {});
    const editor = createEditor({
    extensions: [ArticleSchema, ArticleExtension] as const,
    initialValue: [{ type: "paragraph", children: [{ text: "" }] }],
    });
    });
    const canEditLink = editor.read((state) => state.links.hasSelection());
    editor.update.links.setHref("https://example.com");
    editor.update((tx) => {
    tx.links.setHref("https://example.com");
    });
    ""
    }],
    });
    },
    }),
    });
    ({ signal: context.signal }));
    },
    validate(context) {
    validateTableContributions(context.getContributions(tableContributions));
    },
    });
    },
    } as const;
    const tableCell = schema.handle.element(tableModel, "tableCell");
    const colSpan = schema.handle.property(tableCell, "colSpan");
    const TablesExtension = defineExtension("tables", {
    ...tableModel,
    read: ({ state }) => ({
    selectedCellColSpan(element) {
    return state.schema.getElementProperty(element, colSpan);
    },
    }),
    });
    }),
    });
    }),
    });
    type ImagesDefinition = DefinitionOf<typeof ImagesExtension>;