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
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
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,
],
});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.
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),
}),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),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,
}));
},Plugin for managing tab order between tabbable elements.
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.
() => trueAdd event listener to document instead of editor.
falseAdd additional tabbable entries outside editor.
() => []Determine if element should be tabbable.
({ editor, entry }) => editor.read.schema.isVoid(entry.slateNode)The context every callback receives.
Defines the properties of a tabbable entry.
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),
},
}),
],
});