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

Applying Custom Formatting

PreviousNext

Toggle marks and render inline formatting with current Plite transactions.

Block types change whole elements. Marks change text. Use marks for inline formatting such as bold, italic, code, and strikethrough.

Starting Point

Start with the block renderer from the previous walkthrough:

const renderElement = (props) => {
  switch (props.element.type) {
    case "code":
      return <CodeElement {...props} />;
    default:
      return <DefaultElement {...props} />;
  }
};
Defining Custom ElementsExecuting Commands

On This Page

Starting PointToggle A MarkRender Marks
Build your editor
Production-ready AI template and reusable components.
Get all-access
const renderElement = (props) => {
  switch (props.element.type) {
    case "code":
      return <CodeElement {...props} />;
    default:
      return <DefaultElement {...props} />;
  }
};

Toggle A Mark

Use editor.update.marks.toggle(...) for a single formatting command.

import { ElementApi } from "@platejs/plite";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }
 
          switch (event.key) {
            case "`": {
              event.preventDefault();
              const match = editor.read((state) =>
                state.nodes.find({
                  match: (node) =>
                    ElementApi.isElement(node) && node.type === "code",
                })
              );
              editor.update((tx) => {
                tx.blocks.set({ type: match ? "paragraph" : "code" });
              });
              break;
            }
 
            case "b": {
              event.preventDefault();
              editor.update.marks.toggle("bold");
              break;
            }
          }
        }}
      />
    </Plite>
  );
};
import { ElementApi } from "@platejs/plite";
 
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }
 
          switch (event.key) {
            case "`": {
              event.preventDefault();
              const





















editor.update.marks.toggle('bold') applies the mark to the selected range. When the selection is collapsed, it changes the active mark for text typed next.

Declare mutually exclusive marks in the schema:

const ScriptPosition = schema.property.exclusive("app:script-position");
 
const formattingProperties = [
  schema.textProperty("subscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
  schema.textProperty("superscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
];
 
editor.update.marks.toggle("subscript");
const ScriptPosition = schema.property.exclusive("app:script-position");
 
const formattingProperties = [
  schema.textProperty("subscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
  schema.textProperty("superscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
];
 
editor.update.marks.toggle("subscript");

Enabling one group member removes the active peer automatically.

Render Marks

Add renderLeaf to decide how marked text appears.

const renderLeaf = ({ attributes, children, leaf }) => {
  return (
    <span
      {...attributes}
      style={{
        fontWeight: leaf.bold ? "bold" : "normal",
      }}
    >
      {children}
    </span>
  );
};
const renderLeaf = ({ attributes, children, leaf }) => {
  return (
    <span
      {...attributes}
      style={{
        fontWeight: leaf.bold ? "bold" : "normal",
      }}
    >
      {children}
    </span>
  );
};

Pass both renderers to Editable:

const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }
 
          switch (event.key) {
            case "`": {
              event.preventDefault();
              const match = editor.read((state) =>
                state.nodes.find({
                  match: (node) =>
                    ElementApi.isElement(node) && node.type === "code",
                })
              );
              editor.update((tx) => {
                tx.blocks.set({ type: match ? "paragraph" : "code" });
              });
              break;
            }
 
            case "b": {
              event.preventDefault();
              editor.update.marks.toggle("bold");
              break;
            }
          }
        }}
      />
    </Plite>
  );
};
const App = () => {
  const editor = usePliteEditor({ initialValue });
 
  return (
    <Plite editor={editor}>
      <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }
 
          switch (event.key) {
            case "`": {
              event.preventDefault();
              const match = editor.





















Hotkeys like Ctrl+B belong in onKeyDown because they are UI shortcuts. Reusable editing behavior such as deleting, inserting breaks, or inserting text belongs in extension commands so keyboard input, native input, toolbars, programmatic calls, and tests use the same behavior.

match
=
editor.
read
((
state
)
=>
state.nodes.find({
match: (node) =>
ElementApi.isElement(node) && node.type === "code",
})
);
editor.update((tx) => {
tx.blocks.set({ type: match ? "paragraph" : "code" });
});
break;
}
case "b": {
event.preventDefault();
editor.update.marks.toggle("bold");
break;
}
}
}}
/>
</Plite>
);
};
read
((
state
)
=>
state.nodes.find({
match: (node) =>
ElementApi.isElement(node) && node.type === "code",
})
);
editor.update((tx) => {
tx.blocks.set({ type: match ? "paragraph" : "code" });
});
break;
}
case "b": {
event.preventDefault();
editor.update.marks.toggle("bold");
break;
}
}
}}
/>
</Plite>
);
};