The Table Handle component provides interactive controls for tables in your editor. It includes column and row handles that enable adding, deleting, and reordering table elements through drag-and-drop operations.
Features
- Column and row manipulation
- Drag-and-drop reordering
- Context menus for table operations
- Visual drag preview
- Drop indicators
- Automatic positioning
- Framework-agnostic
Installation
import {
TableHandleRoot,
TableHandleColumnRoot,
TableHandleColumnTrigger,
TableHandleRowRoot,
TableHandleRowTrigger,
TableHandlePopoverContent,
TableHandlePopoverItem,
TableHandleDragPreview,
TableHandleDropIndicator,
} from 'prosekit/react/table-handle'
Basic Usage
import { ProseKit } from 'prosekit/react'
import {
TableHandleRoot,
TableHandleColumnRoot,
TableHandleColumnTrigger,
TableHandleRowRoot,
TableHandleRowTrigger,
TableHandleDragPreview,
TableHandleDropIndicator,
} from 'prosekit/react/table-handle'
export default function Editor() {
return (
<ProseKit editor={editor}>
<div ref={editor.mount} />
<TableHandleRoot>
{/* Visual feedback during drag */}
<TableHandleDragPreview />
<TableHandleDropIndicator />
{/* Column handle */}
<TableHandleColumnRoot>
<TableHandleColumnTrigger>
<ColumnIcon />
</TableHandleColumnTrigger>
</TableHandleColumnRoot>
{/* Row handle */}
<TableHandleRowRoot>
<TableHandleRowTrigger>
<RowIcon />
</TableHandleRowTrigger>
</TableHandleRowRoot>
</TableHandleRoot>
</ProseKit>
)
}
Components
TableHandleRoot
Container for all table handle components. Must wrap all other table handle components.
CSS class name for styling.
TableHandleColumnRoot
Container for column handle trigger and popover.
Position of the handle relative to the table cell.
Whether to use the browser Popover API.
CSS class name for styling.
TableHandleColumnTrigger
The draggable trigger that appears above columns.
CSS class name for styling.
TableHandleRowRoot
Container for row handle trigger and popover.
Position of the handle relative to the table cell. Use right for RTL layouts.
Whether to use the browser Popover API.
CSS class name for styling.
TableHandleRowTrigger
The draggable trigger that appears beside rows.
CSS class name for styling.
TableHandlePopoverContent
Container for menu items that appears when clicking a handle.
CSS class name for styling.
TableHandlePopoverItem
Individual menu item.
Callback executed when the item is selected.
CSS class name for styling.
TableHandleDragPreview
Visual preview of the column/row being dragged. Should be rendered once.
CSS class name for styling.
TableHandleDropIndicator
Visual indicator showing where the dragged column/row will be dropped. Should be rendered once.
CSS class name for styling.
Complete Example
import { useEditor, useEditorDerivedValue } from 'prosekit/react'
import {
TableHandleRoot,
TableHandleColumnRoot,
TableHandleColumnTrigger,
TableHandleRowRoot,
TableHandleRowTrigger,
TableHandlePopoverContent,
TableHandlePopoverItem,
TableHandleDragPreview,
TableHandleDropIndicator,
} from 'prosekit/react/table-handle'
function getTableCommands(editor: Editor) {
return {
// Column commands
addColumnBefore: {
canExec: editor.commands.addTableColumnBefore.canExec(),
command: () => editor.commands.addTableColumnBefore(),
},
addColumnAfter: {
canExec: editor.commands.addTableColumnAfter.canExec(),
command: () => editor.commands.addTableColumnAfter(),
},
deleteColumn: {
canExec: editor.commands.deleteTableColumn.canExec(),
command: () => editor.commands.deleteTableColumn(),
},
// Row commands
addRowAbove: {
canExec: editor.commands.addTableRowAbove.canExec(),
command: () => editor.commands.addTableRowAbove(),
},
addRowBelow: {
canExec: editor.commands.addTableRowBelow.canExec(),
command: () => editor.commands.addTableRowBelow(),
},
deleteRow: {
canExec: editor.commands.deleteTableRow.canExec(),
command: () => editor.commands.deleteTableRow(),
},
// General
clearContents: {
canExec: editor.commands.deleteCellSelection.canExec(),
command: () => editor.commands.deleteCellSelection(),
},
deleteTable: {
canExec: editor.commands.deleteTable.canExec(),
command: () => editor.commands.deleteTable(),
},
}
}
export default function TableHandle({ dir = 'ltr' }) {
const editor = useEditor()
const commands = useEditorDerivedValue(getTableCommands)
return (
<TableHandleRoot>
<TableHandleDragPreview />
<TableHandleDropIndicator />
{/* Column handle */}
<TableHandleColumnRoot className="table-column-handle">
<TableHandleColumnTrigger className="table-column-trigger">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M3 4h10M3 8h10M3 12h10" stroke="currentColor" />
</svg>
</TableHandleColumnTrigger>
<TableHandlePopoverContent className="table-handle-menu">
{commands.addColumnBefore.canExec && (
<TableHandlePopoverItem
onSelect={commands.addColumnBefore.command}
className="table-menu-item"
>
Insert Left
</TableHandlePopoverItem>
)}
{commands.addColumnAfter.canExec && (
<TableHandlePopoverItem
onSelect={commands.addColumnAfter.command}
className="table-menu-item"
>
Insert Right
</TableHandlePopoverItem>
)}
{commands.clearContents.canExec && (
<TableHandlePopoverItem
onSelect={commands.clearContents.command}
className="table-menu-item"
>
Clear Contents
</TableHandlePopoverItem>
)}
{commands.deleteColumn.canExec && (
<TableHandlePopoverItem
onSelect={commands.deleteColumn.command}
className="table-menu-item"
>
Delete Column
</TableHandlePopoverItem>
)}
{commands.deleteTable.canExec && (
<TableHandlePopoverItem
onSelect={commands.deleteTable.command}
className="table-menu-item danger"
>
Delete Table
</TableHandlePopoverItem>
)}
</TableHandlePopoverContent>
</TableHandleColumnRoot>
{/* Row handle */}
<TableHandleRowRoot
placement={dir === 'rtl' ? 'right' : 'left'}
className="table-row-handle"
>
<TableHandleRowTrigger className="table-row-trigger">
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M4 3v10M8 3v10M12 3v10" stroke="currentColor" />
</svg>
</TableHandleRowTrigger>
<TableHandlePopoverContent className="table-handle-menu">
{commands.addRowAbove.canExec && (
<TableHandlePopoverItem
onSelect={commands.addRowAbove.command}
className="table-menu-item"
>
Insert Above
</TableHandlePopoverItem>
)}
{commands.addRowBelow.canExec && (
<TableHandlePopoverItem
onSelect={commands.addRowBelow.command}
className="table-menu-item"
>
Insert Below
</TableHandlePopoverItem>
)}
{commands.clearContents.canExec && (
<TableHandlePopoverItem
onSelect={commands.clearContents.command}
className="table-menu-item"
>
Clear Contents
</TableHandlePopoverItem>
)}
{commands.deleteRow.canExec && (
<TableHandlePopoverItem
onSelect={commands.deleteRow.command}
className="table-menu-item"
>
Delete Row
</TableHandlePopoverItem>
)}
{commands.deleteTable.canExec && (
<TableHandlePopoverItem
onSelect={commands.deleteTable.command}
className="table-menu-item danger"
>
Delete Table
</TableHandlePopoverItem>
)}
</TableHandlePopoverContent>
</TableHandleRowRoot>
</TableHandleRoot>
)
}
Styling
.table-column-handle,
.table-row-handle {
position: absolute;
}
.table-column-trigger,
.table-row-trigger {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 4px;
cursor: grab;
transition: all 0.15s;
}
.table-column-trigger:hover,
.table-row-trigger:hover {
background: #f3f4f6;
}
.table-column-trigger:active,
.table-row-trigger:active {
cursor: grabbing;
}
.table-handle-menu {
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 4px;
min-width: 160px;
}
.table-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
width: 100%;
text-align: left;
}
.table-menu-item:hover {
background: #f3f4f6;
}
.table-menu-item.danger {
color: #dc2626;
}
.table-menu-item.danger:hover {
background: #fee2e2;
}
Table Commands
The table handle uses these editor commands:
Column Operations
addTableColumnBefore() - Insert column to the left
addTableColumnAfter() - Insert column to the right
deleteTableColumn() - Delete the current column
Row Operations
addTableRowAbove() - Insert row above
addTableRowBelow() - Insert row below
deleteTableRow() - Delete the current row
General Operations
deleteCellSelection() - Clear selected cells’ contents
deleteTable() - Delete the entire table
Drag and Drop
Column Reordering
- Click and hold the column trigger
- Drag horizontally
- Drop indicator shows target position
- Release to reorder
Row Reordering
- Click and hold the row trigger
- Drag vertically
- Drop indicator shows target position
- Release to reorder
RTL Support
For right-to-left languages, position row handles on the right:
<TableHandleRowRoot placement="right">
{/* ... */}
</TableHandleRowRoot>
Accessibility
- Keyboard accessible menus
- ARIA labels for handles
- Focus management
- Screen reader support
Best Practices
-
Always include both preview and indicator: These provide essential visual feedback during drag operations
-
Conditional menu items: Only show commands that can be executed in the current context
-
Destructive actions: Visually distinguish dangerous actions like “Delete Table”
-
RTL support: Adjust row handle placement for right-to-left languages
-
Mobile: Consider disabling drag-and-drop on touch devices or providing an alternative UI
Common Issues
Handles don’t appear
- Ensure
TableHandleRoot wraps all components
- Verify the table extension is installed
- Check CSS isn’t hiding the handles
Drag doesn’t work
- Make sure both
TableHandleDragPreview and TableHandleDropIndicator are rendered
- Verify drag events aren’t being prevented
- Check that
TableHandlePopoverContent is inside the appropriate Root component
- Verify commands are available (check
canExec)
See Also