From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Tabbable

PreviousNext

Maintain a consistent tab order for tabbable elements.

Loading…
Navigation FeedbackToolbar

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginAdvanced UsageConflicts with Other PluginsNon-void Plite NodesDOM Elements Outside the EditorPluginsTabbablePluginRead APIeditor.plugin(TabbablePlugin).read.findDestination(options)TypesTabbableEntry
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Ensures consistent tab order between tabbable elements in the editor
  • Manages focus transitions between void elements and external DOM elements
Report an issue

Kit Usage

Installation

The fastest way to add the tabbable plugin is with TabbableKit. The kit keeps Indent installed and removes only its Tab and Shift+Tab shortcuts so focus navigation owns those keys.

'use client';
 
import type { TabbablePluginState } from '@platejs/tabbable';
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS, ElementApi } from 'platejs';
 
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
 
export const TabbableKit = [
  TabbablePlugin.extend({
    override: {
      plugins: {
        [PLUGINS.indent]: {
          shortcuts: {
            tab: null,
            untab: null,
          },
        },
      },
    },
  }).extend(({ editor }): { initialState: TabbableKitPluginState } => ({
    initialState: {
      query: () => {
        if (
          editor.read.selection.isAtBlockStart() ||
          editor.read.selection.isAtBlockEnd()
        ) {
          return false;
        }
 
        const blockingTypes = new Set(
          [
            PLUGINS.codeBlock,
            PLUGINS.listItem,
            PLUGINS.todoList,
            PLUGINS.table,
          ].flatMap((name) => {
            const plugin = editor.plugin(name);
 
            return plugin.installed ? [plugin.schema.type] : [];
          })
        );
 
        return !editor.read.nodes.some({
          match: (n) =>
            !!(
              (ElementApi.isElement(n) && blockingTypes.has(n.type)) ||
              (ElementApi.isElement(n) && n.listType)
            ),
        });
      },
    },
  })),
];
'use client';
 
import type { TabbablePluginState } from '@platejs/tabbable';
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS, ElementApi } from 'platejs';
 
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
 
export const TabbableKit = [
  TabbablePlugin.extend({
    override: {
      plugins: {
        [PLUGINS.indent]: {
          shortcuts: {
            tab: null,
            untab: null,
          },
        },



































Add Kit

import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/tabbable';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...TabbableKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { TabbableKit } from '@/components/editor/tabbable';
 
const editor = createPlateEditor




Manual Usage

Installation

pnpm add @platejs/tabbable
pnpm add @platejs/tabbable

Add Plugin

import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin,
  ],
});
import { TabbablePlugin } from '@platejs/tabbable/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin,
  ],
});

Configure Plugin

import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin.configure(({ editor }) => {
      const listItem = editor.plugin(PLUGINS.listItem);
      const codeBlock = editor.plugin(PLUGINS.codeBlock);
      const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
        plugin.installed 














  • initialState.query: Function to dynamically enable/disable the plugin based on editor state.
  • initialState.globalEventListener: When true, adds event listener to document instead of editor.
  • initialState.isTabbable: Function to determine which elements should be included in tab order.

Advanced Usage

Conflicts with Other Plugins

The Tabbable plugin may cause issues with other plugins that handle the Tab key, such as:

  • Lists
  • Code blocks
  • Indent plugin

Use the query option to disable the Tabbable plugin when the Tab key should be handled by another plugin:

TabbablePlugin.configure(({ editor }) => {
  const listItem = editor.plugin(PLUGINS.listItem);
  const codeBlock = editor.plugin(PLUGINS.codeBlock);
  const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
    plugin.installed ? [plugin.schema.type] : []
  );
 
  return {
    initialState: {
      query: () =>
        blockedTypes.length === 0 ||
        !editor.read.nodes.some({
          type: blockedTypes,
        }),
    },
  };
});
TabbablePlugin.configure(({ editor }) => {
  const listItem = editor.plugin(PLUGINS.listItem);
  const codeBlock = editor.plugin(PLUGINS.codeBlock);
  const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
    plugin.installed ? [plugin.schema.type] : []
  );
 
  return {
    initialState: {
      query: () =>
        blockedTypes.length === 0 ||
        !editor.read.nodes.some({
          type: blockedTypes,
        }),


Alternatively, if you're using the Indent plugin, you can enable the Tabbable plugin only when a specific type of node is selected, such as voids:

query: (event) => !!editor.read.nodes.some({
  match: (node) => editor.read.schema.isVoid(node),
}),
query: (event) => !!editor.read.nodes.some({
  match: (node) => editor.read.schema.isVoid(node),
}),

Non-void Plite Nodes

One TabbableEntry will be created for each tabbable DOM element in the editor, as determined using the tabbable NPM package. The list of tabbables is then filtered using isTabbable.

By default, isTabbable only returns true for entries inside void Plite nodes. You can override isTabbable to add support for DOM elements contained in other types of Plite node:

// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: (tabbableEntry) => (
  tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
  editor.read.schema.isVoid(tabbableEntry.slateNode)
),
// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: (tabbableEntry) => (
  tabbableEntry.slateNode.type === CUSTOM_ELEMENT ||
  editor.read.schema.isVoid(tabbableEntry.slateNode)
),

DOM Elements Outside the Editor

In some circumstances, you may want to allow users to tab from the editor to a DOM element rendered outside the editor, such as an interactive popover.

To do this, override insertTabbableEntries to return an array of TabbableEntry objects, one for each DOM element outside the editor that you want to include in the tabbable list. The slateNode and path of the TabbableEntry should refer to the Plite node the user's cursor will be inside when the DOM element should be tabbable to.

Set the globalEventListener option to true to make sure the Tabbable plugin is able to return the user's focus to the editor.

For example, if the DOM element appears when a link is selected, the slateNode and path should be that of the link.

// Add buttons inside .my-popover to the list of tabbables
globalEventListener: true,
insertTabbableEntries: () => {
  const selectedEntry = editor.read.nodes.above();
 
  if (!selectedEntry) return [];
 
  const [selectedNode, selectedNodePath] = selectedEntry;
 
  return [
    ...document.querySelectorAll('.my-popover > button'),
  ].map((domNode) => ({
    domNode,
    slateNode: selectedNode,
    path: selectedNodePath,
  }));
},
// Add buttons inside .my-popover to the list of tabbables
globalEventListener: true,
insertTabbableEntries: () => {
  const selectedEntry = editor.read.nodes.above();
 
  if (!selectedEntry) return [];
 
  const [selectedNode, selectedNodePath] = selectedEntry;
 
  return [
    ...document.querySelectorAll('.my-popover > button'),
  ].map((domNode) => ({
    domNode,
    slateNode: selectedNode,
    path: selectedNodePath,
  }));
},

Plugins

TabbablePlugin

Plugin for managing tab order between tabbable elements.

Options

    Enable/disable plugin dynamically.

    • Default: () => true

    Add event listener to document instead of editor.

    • Default: false

    Add additional tabbable entries outside editor.

    • Default: () => []

    Determine if element should be tabbable.

    • Default: (tabbableEntry) => editor.read.schema.isVoid(tabbableEntry.slateNode)

Read API

editor.plugin(TabbablePlugin).read.findDestination(options)

Finds the next DOM or document destination from the active snapshot.

Types

TabbableEntry

Defines the properties of a tabbable entry.

Attributes

    Focusable HTML or SVG element representing the tabbable entry.

    Corresponding Plite node.

    Path to Plite node in document.

},
},
}).extend(({ editor }): { initialState: TabbableKitPluginState } => ({
initialState: {
query: () => {
if (
editor.read.selection.isAtBlockStart() ||
editor.read.selection.isAtBlockEnd()
) {
return false;
}
const blockingTypes = new Set(
[
PLUGINS.codeBlock,
PLUGINS.listItem,
PLUGINS.todoList,
PLUGINS.table,
].flatMap((name) => {
const plugin = editor.plugin(name);
return plugin.installed ? [plugin.schema.type] : [];
})
);
return !editor.read.nodes.some({
match: (n) =>
!!(
(ElementApi.isElement(n) && blockingTypes.has(n.type)) ||
(ElementApi.isElement(n) && n.listType)
),
});
},
},
})),
];
({
plugins: [
// ...otherPlugins,
...TabbableKit,
],
});
?
[plugin.schema.type]
:
[]
);
return {
initialState: {
query: () =>
blockedTypes.length === 0 ||
!editor.read.nodes.some({ type: blockedTypes }),
globalEventListener: true,
isTabbable: (tabbableEntry) =>
editor.read.schema.isVoid(tabbableEntry.slateNode),
},
};
}),
],
});
import { TabbablePlugin } from '@platejs/tabbable/react';
import { PLUGINS } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin.configure(({ editor }) => {
      const listItem = editor.plugin(PLUGINS.listItem);
      const codeBlock = editor.plugin(PLUGINS.codeBlock);
      const blockedTypes = [listItem, codeBlock].flatMap((plugin) =>
        plugin.installed ? [plugin.schema.type] : []
      );
 
      return {
        initialState: {
          query: () =>
            blockedTypes.length === 0 ||
            !editor.read.nodes.some({ type: blockedTypes }),
          globalEventListener: true,
          isTabbable: (tabbableEntry) =>
            editor.read.schema.isVoid(tabbableEntry.slateNode),
        },
      };
    }),
  ],
});
},
};
});