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
    • Linting
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • DOCX
    • HTML
    • Markdown

Tabbable

PreviousNext

Maintain a consistent tab order for tabbable elements.

Loading…
LintingToolbar

On This Page

FeaturesKit usageInstallationAdd kitManual usageInstallationAdd pluginConfigure pluginAdvanced usageConflicts with other pluginsNon-void Plate nodesDOM elements outside the editorPluginsTabbablePluginTypesTabbableEventContextTabbableEntry
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 turns off its Tab and Shift+Tab shortcuts, so Tab in a paragraph moves focus instead of indenting. Its query declines in list items, code blocks and tables, which handle Tab themselves.

'use client';
 
import { BaseCodeBlockPlugin, PLUGINS, ElementApi } from 'platejs';
import { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseTablePlugin } from 'platejs/table';
 
export const TabbableKit = [
  TabbablePlugin.configure({
    override: {
      [PLUGINS.indent]: {
        shortcuts: {
          tab: null,
          untab: null,
        },
      },
    },
    initialState: {
      query: ({ editor }) => {
        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 { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseTablePlugin } from 'platejs/table';
 
export const TabbableKit = [
  TabbablePlugin.configure({
    override: {
      [PLUGINS.indent]: {
        shortcuts: {
          tab: null,
          untab: null,
        },
      },
    },
    initialState: {
      query: ({ editor }) => {
        const blockingTypes

















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({
      initialState: {
        query: ({ editor }) => {
          const codeBlock = editor.plugin(BaseCodeBlockPlugin);
 
          return (
            !codeBlock.installed ||
            !editor.read.nodes.some({ type: codeBlock.schema.type })
          );







  • initialState.query: Decides whether Tabbable handles a Tab. A Tab pressed on a stop Tabbable admits, which is an inserted entry or an in-editor tabbable that isTabbable accepts, skips query and always follows the tab order.
  • 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.

Each callback receives the editor of the view Tabbable runs in. Read the caret from that editor, not from an editor captured in .configure, which may not hold the caret of that view.

Advanced usage

Conflicts with other plugins

Tabbable handles Tab before any editor shortcut. Another feature gets the key when query declines or when no stop follows the caret. Lists, code blocks, tables and the Indent plugin all use Tab. Lists take Tab and Shift+Tab on list items themselves, even with Indent's shortcuts turned off.

Use the query option to decline where another feature should handle the Tab key:

TabbablePlugin.configure({
  initialState: {
    query: ({ editor }) => {
      const codeBlock = editor.plugin(BaseCodeBlockPlugin);
 
      return (
        !codeBlock.installed ||
        !editor.read.nodes.some({ type: codeBlock.schema.type })
      );
    },
  },
});
TabbablePlugin.configure({
  initialState: {
    query: ({ editor }) => {
      const codeBlock = editor.plugin(BaseCodeBlockPlugin);
 
      return (
        !codeBlock.installed ||
        !editor.read.nodes.some({ type: codeBlock.schema.type })
      );
    },
  },
});

Extend query for any other feature in your editor that uses Tab, such as a Copilot suggestion or an open combobox. TabbableKit declines only in list items, code blocks and tables.

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: ({ editor }) =>
  editor.read.nodes.some({
    match: (node) => editor.read.schema.isVoid(node),
  }),
query: ({ editor }) =>
  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: ({ editor, entry }) =>
  entry.slateNode.type === CUSTOM_ELEMENT ||
  editor.read.schema.isVoid(entry.slateNode),
// Enable tabbable DOM elements inside CUSTOM_ELEMENT
isTabbable: ({ editor, entry }) =>
  entry.slateNode.type === CUSTOM_ELEMENT ||
  editor.read.schema.isVoid(entry.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: ({ editor }) => {
  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: ({ editor }) => {
  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

    Decide whether Tabbable handles a Tab. A Tab pressed on a stop Tabbable admits, which is an inserted entry or an in-editor tabbable that isTabbable accepts, skips query and always follows the tab order.

    • 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: ({ editor, entry }) => editor.read.schema.isVoid(entry.slateNode)

Types

TabbableEventContext

The context every callback receives.

Attributes

    Editor of the view Tabbable runs in. It holds that view's caret.

    The Tab keydown event.

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.

=
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,
],
});
},
globalEventListener: true,
isTabbable: ({ editor, entry }) =>
editor.read.schema.isVoid(entry.slateNode),
},
}),
],
});
import { TabbablePlugin } from 'platejs/tabbable/react';
import { BaseCodeBlockPlugin } from 'platejs';
import { createEditor } from 'platejs/react';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    TabbablePlugin.configure({
      initialState: {
        query: ({ editor }) => {
          const codeBlock = editor.plugin(BaseCodeBlockPlugin);
 
          return (
            !codeBlock.installed ||
            !editor.read.nodes.some({ type: codeBlock.schema.type })
          );
        },
        globalEventListener: true,
        isTabbable: ({ editor, entry }) =>
          editor.read.schema.isVoid(entry.slateNode),
      },
    }),
  ],
});