Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Comments
  • Discussion
  • Suggestions
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Details
  • 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
    • 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 Plate nodesDOM elements outside the editorPluginsTabbablePluginTypesTabbableEntry
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 { BaseCodeBlockPlugin, PLUGINS, ElementApi } from 'platejs';
import type { TabbablePluginState } from 'platejs/tabbable';
import { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseTablePlugin } from 'platejs/table';
 
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
 
export const TabbableKit = [
  TabbablePlugin.extend({
    override: {
      [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<string>(
          [BaseCodeBlockPlugin, BaseTablePlugin].flatMap((descriptor) => {
            const plugin = editor.plugin(descriptor);
 
            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 { BaseCodeBlockPlugin, PLUGINS, ElementApi } from 'platejs';
import type { TabbablePluginState } from 'platejs/tabbable';
import { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseTablePlugin } from 'platejs/table';
 
export type TabbableKitPluginState = Pick<TabbablePluginState, 'query'>;
 
export const TabbableKit = [
  TabbablePlugin.extend({
    override: {
      [PLUGINS.indent]: {
        shortcuts: {
          tab: null,
          untab: null































Add kit

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




Manual usage

Installation

pnpm add platejs @tanstack/react-virtual tabbable
pnpm add platejs @tanstack/react-virtual tabbable

Add plugin

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

Configure plugin

import { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseCodeBlockPlugin } from 'platejs';
import { createEditor } from 'platejs/react';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin.configure(({ editor }) => {
      const codeBlock = editor.plugin(BaseCodeBlockPlugin);
      const blockedTypes = [codeBlock].flatMap((plugin) =>
        plugin.installed ? [plugin.schema.type] : []
      );
 
      return {











  • 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 codeBlock = editor.plugin(BaseCodeBlockPlugin);
  const blockedTypes = [codeBlock].flatMap((plugin) =>
    plugin.installed ? [plugin.schema.type] : []
  );
 
  return {
    initialState: {
      query: () =>
        blockedTypes.length === 0 ||
        !editor.read.nodes.some({
          type: blockedTypes,
        }),
    },
  };
});
TabbablePlugin.configure(({ editor }) => {
  const codeBlock = editor.plugin(BaseCodeBlockPlugin);
  const blockedTypes = [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 Plate 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 Plate nodes. You can override isTabbable to add support for DOM elements contained in other types of Plate 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 Plate 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)

Types

TabbableEntry

Defines the properties of a tabbable entry.

Attributes

    Focusable HTML or SVG element representing the tabbable entry.

    Corresponding Plate node.

    Path to Plate node in document.

,
},
},
},
}).extend(({ editor }): { initialState: TabbableKitPluginState } => ({
initialState: {
query: () => {
if (
editor.read.selection.isAtBlockStart() ||
editor.read.selection.isAtBlockEnd()
) {
return false;
}
const blockingTypes = new Set<string>(
[BaseCodeBlockPlugin, BaseTablePlugin].flatMap((descriptor) => {
const plugin = editor.plugin(descriptor);
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,
],
});
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 { BaseCodeBlockPlugin } from 'platejs';
import { createEditor } from 'platejs/react';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin.configure(({ editor }) => {
      const codeBlock = editor.plugin(BaseCodeBlockPlugin);
      const blockedTypes = [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),
        },
      };
    }),
  ],
});