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

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 CodeElement = ({ attributes, children }) => (
  <pre {...attributes}><code>{children}</code></pre>
);
 
const DefaultElement = ({ attributes, children }) => (
  <









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
p
{
...
attributes}>{children}</
p
>
);
const renderElement = (props) => {
switch (props.element.type) {
case "code":
return <CodeElement {...props} />;
default:
return <DefaultElement {...props} />;
}
};
const CodeElement = ({ attributes, children }) => (
  <pre {...attributes}><code>{children}</code></pre>
);
 
const DefaultElement = ({ attributes, children }) => (
  <p {...attributes}>{children}</p>
);
 
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 "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot 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;
            }
          }
        }}
      />
    </EditorRoot>
  );
};
import { ElementApi } from "plitejs";
 
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot 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:

import { definePlugin, property, schema } from "plitejs";
 
const ScriptPosition = schema.property.exclusive("app:script-position");
 
const formattingProperties = [
  schema.textProperty("subscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
  schema.textProperty("superscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
];
 
const FormattingPlugin = definePlugin("formatting", {
  schema: { properties: formattingProperties },
});
 
// Inside the component that owns this editor:
const editor = useEditor({ plugins: [FormattingPlugin], initialValue });
editor.update.marks.toggle("subscript");
import { definePlugin, property, schema } from "plitejs";
 
const ScriptPosition = schema.property.exclusive("app:script-position");
 
const formattingProperties = [
  schema.textProperty("subscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
  schema.textProperty("superscript", property.boolean(), {
    exclusive: [ScriptPosition],
  }),
];
 
const FormattingPlugin = definePlugin("formatting", {
  schema: { properties: formattingProperties },
});
 
// Inside the component that owns this editor:

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 = useEditor({ initialValue });
 
  return (
    <EditorRoot 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;
            }
          }
        }}
      />
    </EditorRoot>
  );
};
const App = () => {
  const editor = useEditor({ initialValue });
 
  return (
    <EditorRoot editor={editor}>
      <Editable
        renderElement={renderElement}
        renderLeaf={renderLeaf}
        onKeyDown={(event) => {
          if (!event.ctrlKey) {
            return;
          }
 
          switch (event.key) {
            case "`": {
              event.preventDefault();
              const match =





















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 plugin 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;
}
}
}}
/>
</EditorRoot>
);
};
const
editor
=
useEditor
({ plugins: [FormattingPlugin], initialValue });
editor.update.marks.toggle("subscript");
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;
}
}
}}
/>
</EditorRoot>
);
};