From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting

Plugin Shortcuts

PreviousNext

Configure keyboard shortcuts on Plate plugins.

Plugin shortcuts map key combinations to plugin methods or explicit handlers. Plate compiles one immutable, ordered table during plugin setup. Each editable root installs one dispatcher for that table. This guide covers linked methods, custom handlers, overrides, priorities, and default shortcut ownership.

How Shortcuts Resolve

Each plugin owns a shortcuts object. At resolution time Plate namespaces every shortcut as ${plugin.name}.${shortcutName}.

When a shortcut has no handler, Plate looks for a same-named plugin method:

  1. One update method exists: Plate infers update.
  2. One API method exists: Plate infers api.
  3. Both exist: set target: 'update' | 'api' to disambiguate.
  4. Neither exists: plugin compilation reports the invalid shortcut.

An explicit target must name an existing route. A custom handler owns dispatch itself, so it cannot also set target.

Plugin MethodsPlugin Context

On This Page

How Shortcuts ResolveLinked Update ShortcutsLinked API ShortcutsCustom HandlersPrevent DefaultConfigure Existing ShortcutsMultiple ShortcutsPriorityEditor-Level ShortcutsDefault ShortcutsAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access

Declare handlerless shortcuts after the method they link to. TypeScript validates the capabilities already carried by that plugin descriptor, so the constructor contribution with update or api comes before the .extend({ shortcuts }) call. This real type dependency is why the example uses a later stage. Custom handlers do not need a linked method.

FieldMeaning
keysKey combination compiled by Plate's realm-aware matcher. Use a string like 'mod+b' or arrays like [[Key.Mod, 'b']].
handlerExplicit callback receiving { editor, event, eventDetails }.
targetOptional `'update'
priorityShortcut-local priority. Defaults to 0.
preventDefaultBrowser-default policy for a handled shortcut. When omitted, Plate calls event.preventDefault() and event.stopPropagation().
nullRemoves that named shortcut from the plugin.

Linked Update Shortcuts

Use a linked update method when the shortcut and plugin update method share a name.

plugins/signature-plugin.tsx
import { Key, definePlatePlugin } from 'platejs/react';
 
export const SignaturePlugin = definePlatePlugin('signature', {
  update: ({ tx }) => ({
    insertSignature: () => {
      tx.text.insert(' - Plate');
    },
  }),
})
  .extend({
    shortcuts: {
      insertSignature: {
        keys: [[Key.Mod, Key.Shift, 's']],
      },
    },
  });
plugins/signature-plugin.tsx
import { Key, definePlatePlugin } from 'platejs/react';
 
export const SignaturePlugin = definePlatePlugin('signature', {
  update: ({ tx }) => ({
    insertSignature: () => {
      tx.text.insert(' - Plate');
    },
  }),
})
  .extend({
    shortcuts: {
      insertSignature: {
        keys: [[Key.Mod, Key.Shift, 's']],
      },
    },
  });

Pressing Mod+Shift+S runs editor.update((tx) => tx.signature.insertSignature()).

Linked API Shortcuts

When only the matching API method exists, Plate infers the API route.

plugins/inspect-plugin.tsx
import { DebugPlugin } from 'platejs';
import { Key, definePlatePlugin } from 'platejs/react';
 
export const InspectPlugin = definePlatePlugin('inspect', {
  api: ({ editor }) => ({
    logText: () => {
      editor
        .plugin(DebugPlugin)
        .api.info('Editor text', undefined, editor.read.text.string([]));
    },
  }),
})
  .extend({
    shortcuts: {
      logText: {
        keys: [[Key.Mod, Key.Alt, 'l']],
      },
    },
  });
plugins/inspect-plugin.tsx
import { DebugPlugin } from 'platejs';
import { Key, definePlatePlugin } from 'platejs/react';
 
export const InspectPlugin = definePlatePlugin('inspect', {
  api: ({ editor }) => ({
    logText: () => {
      editor
        .plugin(DebugPlugin)
        .api.info('Editor text', undefined, editor.read.text.string([]));
    },
  }),
})
  .extend({
    shortcuts: {
      logText: {
        keys: [[Key.Mod, Key.Alt, 'l']],


Pressing Mod+Alt+L calls editor.plugin(InspectPlugin).api.logText().

Disambiguate collisions

If an update method and API method share a name, set target: 'update' or target: 'api'. Plate never changes route silently when a second method is added. Call plugin-owned APIs through editor.plugin(Plugin).api.

Custom Handlers

Use a handler when the shortcut needs the keyboard event, custom branching, or work that should not live as a plugin API or update method. Do not set target on a handler shortcut.

plugins/draft-plugin.tsx
import { DebugPlugin } from 'platejs';
import { Key, definePlatePlugin } from 'platejs/react';
 
export const DraftPlugin = definePlatePlugin('draft', {
  shortcuts: {
    saveDraft: {
      keys: [[Key.Mod, 's']],
      handler: ({ editor }) => {
        const text = editor.read.text.string([]);
 
        if (text.trim().length === 0) return false;
 
        editor.plugin(DebugPlugin).api.info('Draft text', undefined, text);
 
        return true;
      },
    },
  },
});
plugins/draft-plugin.tsx
import { DebugPlugin } from 'platejs';
import { Key, definePlatePlugin } from 'platejs/react';
 
export const DraftPlugin = definePlatePlugin('draft', {
  shortcuts: {
    saveDraft: {
      keys: [[Key.Mod, 's']],
      handler: ({ editor }) => {
        const text = editor.read.text.string([]);
 
        if (text.trim().length === 0) return false;
 
        editor.plugin(DebugPlugin).api.info('Draft text', undefined, text);
 




Returning false means "not handled"; Plate will not call preventDefault() for that key press. Returning true or undefined means handled when preventDefault is omitted.

Prevent Default

ConfigurationBehavior
preventDefault omitted and handler returns anything except falsePlate calls event.preventDefault() and event.stopPropagation().
Handler returns falsePlate leaves the event alone and tries the next matching shortcut.
preventDefault: false and handler handles the eventPlate stops shortcut dispatch and leaves the browser default enabled.
preventDefault: true, or a predicate returning truePlate calls event.preventDefault().

Use the default omission for normal editor commands.

Configure Existing Shortcuts

Configure a named shortcut to change its keys.

plugins/basic-marks.tsx
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { Key } from 'platejs/react';
 
export const AppBoldPlugin = BoldPlugin.configure({
  shortcuts: {
    toggle: {
      keys: [[Key.Mod, Key.Shift, 'b']],
    },
  },
});
plugins/basic-marks.tsx
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { Key } from 'platejs/react';
 
export const AppBoldPlugin = BoldPlugin.configure({
  shortcuts: {
    toggle: {
      keys: [[Key.Mod, Key.Shift, 'b']],
    },
  },
});

Set a shortcut to null to remove it.

plugins/basic-marks.tsx
import { ItalicPlugin } from '@platejs/basic-nodes/react';
 
export const AppItalicPlugin = ItalicPlugin.configure({
  shortcuts: {
    toggle: null,
  },
});
plugins/basic-marks.tsx
import { ItalicPlugin } from '@platejs/basic-nodes/react';
 
export const AppItalicPlugin = ItalicPlugin.configure({
  shortcuts: {
    toggle: null,
  },
});

The null value removes italic.toggle from the compiled shortcut set.

Multiple Shortcuts

A plugin can declare multiple shortcut names. Keep each name aligned with the method it should call.

plugins/review-plugin.tsx
import { Key, definePlatePlugin } from 'platejs/react';
 
export const ReviewPlugin = definePlatePlugin('review', {
  update: ({ tx }) => ({
    accept: () => tx.text.insert('Accepted'),
    reject: () => tx.text.insert('Rejected'),
  }),
})
  .extend({
    shortcuts: {
      accept: {
        keys: [[Key.Mod, Key.Alt, 'a']],
      },
      reject: {
        keys: [[Key.Mod, Key.Alt, 'r']],
      },
    },
  });
plugins/review-plugin.tsx
import { Key, definePlatePlugin } from 'platejs/react';
 
export const ReviewPlugin = definePlatePlugin('review', {
  update: ({ tx }) => ({
    accept: () => tx.text.insert('Accepted'),
    reject: () => tx.text.insert('Rejected'),
  }),
})
  .extend({
    shortcuts: {
      accept: {
        keys: [[Key.Mod, Key.Alt, 'a']],
      },
      reject: {
        keys: [[Key.Mod, Key.Alt, 'r']],
      },
    },

This creates the compiled shortcut IDs review.accept and review.reject.

Priority

Shortcut priority defaults to 0. Set it on a shortcut when two handlers use the same key combination and one should win.

plugins/priority-plugin.tsx
import { definePlatePlugin } from 'platejs/react';
 
export const PriorityPlugin = definePlatePlugin('commandMenu', {
  shortcuts: {
    openCommandMenu: {
      keys: 'mod+k',
      priority: 200,
      handler: ({ editor }) => {
        editor.api.debug.info('Open command menu');
 
        return true;
      },
    },
  },
});
plugins/priority-plugin.tsx
import { definePlatePlugin } from 'platejs/react';
 
export const PriorityPlugin = definePlatePlugin('commandMenu', {
  shortcuts: {
    openCommandMenu: {
      keys: 'mod+k',
      priority: 200,
      handler: ({ editor }) => {
        editor.api.debug.info('Open command menu');
 
        return true;
      },
    },
  },
});

Plate orders the compiled table by shortcut priority, plugin application order, then declaration order. The first handled route wins.

Editor-Level Shortcuts

createPlateEditor({ shortcuts }) attaches shortcuts to the root plugin. Use it for editor-wide commands that do not belong to one feature plugin.

editor.ts
import { DebugPlugin } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  shortcuts: {
    reportWordCount: {
      keys: 'mod+shift+w',
      handler: ({ editor }) => {
        const words = editor.read.text
          .string([])
          .trim()
          .split(/\s+/)
          .filter(Boolean);
 
        editor
          .plugin(DebugPlugin)
          .api.info('Word count', undefined, words.length);
 
        return true;
      },
    },
  },
});
editor.ts
import { DebugPlugin } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  shortcuts: {
    reportWordCount: {
      keys: 'mod+shift+w',
      handler: ({ editor }) => {
        const words = editor.read.text
          .string([])
          .trim()
          .split(/\s+/)
          .filter(Boolean);
 
        editor
          .plugin(DebugPlugin)
          .api.





Internally this becomes a root shortcut, so plugin-owned shortcuts are still the better fit for feature-owned behavior.

Default Shortcuts

PluginShortcut nameKeys
BoldPlugintoggleMod+B
ItalicPlugintoggleMod+I
UnderlinePlugintoggleMod+U
ParagraphPlugintoggleMod+Alt+0, Mod+Shift+0
CopilotPluginacceptTab
CopilotPluginrejectEscape

Other plugins often expose toggle, insert, or feature-specific transforms without default keys. Add shortcuts in your app when those commands should be keyboard-accessible.

API Reference

Shortcut type
type Shortcut = HotkeysOptions & {
  keys?: Keys | null;
  priority?: number;
  target?: "api" | "update";
  handler?: (ctx: {
    editor: PlateEditor;
    event: KeyboardEvent;
    eventDetails: HotkeysEvent;
  }) => boolean | void;
};
Shortcut type
type Shortcut = HotkeysOptions & {
  keys?: Keys | null;
  priority?: number;
  target?: "api" | "update";
  handler?: (ctx: {
    editor: PlateEditor;
    event: KeyboardEvent;
    eventDetails: HotkeysEvent;
  }) => boolean | void;
};

target and handler are mutually exclusive.

Done. Name shortcuts after plugin-specific transforms or API methods by default, and use handlers only when the keyboard event is part of the behavior.

},
},
});
return
true
;
},
},
},
});
});
info
(
'Word count'
,
undefined
, words.
length
);
return true;
},
},
},
});