Shadcn Table
componentdata-grid-keyboard-shortcutssource: ok · hit
Data Grid Keyboard Shortcuts
A keyboard shortcuts dialog component for the data grid showing all available shortcuts
npx shadcn add "https://tablecn.com/r/data-grid-keyboard-shortcuts.json"free
Where it goes
kanban
Interaction
modal
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
ui-primitives@base-ui/react
Registry dependencies
buttondialoginputkbdseparator
Files2 · source cached 10/9/2026, 5:37:09 AM
src/registry/bases/base/components/data-grid/data-grid-keyboard-shortcuts.tsx
"use client";
import * as React from "react";
import { Button } from "@/registry/bases/base/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/registry/bases/base/ui/dialog";
import { useDirection } from "@/registry/bases/base/ui/direction";
import { Input } from "@/registry/bases/base/ui/input";
import { Separator } from "@/registry/bases/base/ui/separator";
import { Kbd, KbdGroup } from "@/registry/bases/radix/ui/kbd";
import { IconPlaceholder } from "@/registry/icons/icon-placeholder";
const SHORTCUT_KEY = "/";
interface ShortcutGroup {
title: string;
shortcuts: Array<{
keys: string[];
description: string;
}>;
}
interface DataGridKeyboardShortcutsProps {
enableSearch?: boolean;
enableUndoRedo?: boolean;
enablePaste?: boolean;
enableRowAdd?: boolean;
enableRowsDelete?: boolean;
}
export const DataGridKeyboardShortcuts = React.memo(
DataGridKeyboardShortcutsImpl,
(prev, next) => {
return (
prev.enableSearch === next.enableSearch &&
prev.enableUndoRedo === next.enableUndoRedo &&
prev.enablePaste === next.enablePaste &&
prev.enableRowAdd === next.enableRowAdd &&
prev.enableRowsDelete === next.enableRowsDelete
);
},
);
function DataGridKeyboardShortcutsImpl({
enableSearch = false,
enableUndoRedo = false,
enablePaste = false,
enableRowAdd = false,
enableRowsDelete = false,
}: DataGridKeyboardShortcutsProps) {
const dir = useDirection();
const [open, setOpen] = React.useState(false);
const [input, setInput] = React.useState("");
const inputRef = React.useRef<HTMLInputElement>(null);
const isMac =
typeof navigator !== "undefined"
? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
: false;
const modKey = isMac ? "⌘" : "Ctrl";
const onOpenChange = React.useCallback((isOpen: boolean) => {
setOpen(isOpen);
if (!isOpen) {
setInput("");
}
}, []);
const onInputChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setInput(event.target.value);
},
[],
);
const shortcutGroups: ShortcutGroup[] = React.useMemo(
() => [
{
title: "Navigation",
shortcuts: [
{
keys: ["↑", "↓", "←", "→"],
description: "Navigate between cells",
},
{
keys: ["Tab"],
description: "Move to next cell (wraps to next row)",
},
{
keys: ["Shift", "Tab"],
description: "Move to previous cell (wraps to previous row)",
},
{
keys: ["Home"],
description: "Move to first column",
},
{
keys: ["End"],
description: "Move to last column",
},
{
keys: [modKey, "↑"],
description: "Move to first row (same column)",
},
{
keys: [modKey, "↓"],
description: "Move to last row (same column)",
},
{
keys: [modKey, "←"],
description: "Move to first column (same row)",
},
{
keys: [modKey, "→"],
description: "Move to last column (same row)",
},
{
keys: [modKey, "Home"],
description: "Move to first cell",
},
{
keys: [modKey, "End"],
description: "Move to last cell",
},
{
keys: ["PgUp"],
description: "Move up one page",
},
{
keys: ["PgDn"],
description: "Move down one page",
},
{
keys: ["⌥", "↑"],
description: "Scroll up one page",
},
{
keys: ["⌥", "↓"],
description: "Scroll down one page",
},
{
keys: ["⌥", "PgUp"],
description: "Scroll left one page of columns",
},
{
keys: ["⌥", "PgDn"],
description: "Scroll right one page of columns",
},
],
},
{
title: "Selection",
shortcuts: [
{
keys: ["Space"],
description: "Toggle row selection (in select column)",
},
{
keys: ["Shift", "↑↓←→"],
description: "Extend selection",
},
{
keys: [modKey, "Shift", "↑"],
description: "Select to top of table",
},
{
keys: [modKey, "Shift", "↓"],
description: "Select to bottom of table",
},
{
keys: [modKey, "Shift", "←"],
description: "Select to first column",
},
{
keys: [modKey, "Shift", "→"],
description: "Select to last column",
},
{
keys: [modKey, "A"],
description: "Select all cells",
},
{
keys: [modKey, "Click"],
description: "Toggle cell selection",
},
{
keys: ["Shift", "Click"],
description: "Select range",
},
{
keys: ["Esc"],
description: "Clear selection",
},
],
},
{
title: "Editing",
shortcuts: [
{
keys: ["Enter"],
description: "Start editing cell",
},
{
keys: ["F2"],
description: "Start editing cell",
},
{
keys: ["Double Click"],
description: "Start editing cell",
},
{
keys: [modKey, "Enter"],
description: "Open link (in URL cell)",
},
...(enableRowAdd
? [
{
keys: ["Shift", "Enter"],
description: "Insert row below",
},
]
: []),
{
keys: [modKey, "C"],
description: "Copy selected cells",
},
{
keys: [modKey, "X"],
description: "Cut selected cells",
},
...(enablePaste
? [
{
keys: [modKey, "V"],
description: "Paste cells",
},
]
: []),
{
keys: ["Delete"],
description: "Clear selected cells",
},
{
keys: ["Backspace"],
description: "Clear selected cells",
},
...(enableRowsDelete
? [
{
keys: [modKey, "Backspace"],
description: "Delete selected rows",
},
{
keys: [modKey, "Delete"],
description: "Delete selected rows",
},
]
: []),
...(enableUndoRedo
? [
{
keys: [modKey, "Z"],
description: "Undo last action",
},
{
keys: [modKey, "Shift", "Z"],
description: "Redo last action",
},
]
: []),
],
},
...(enableSearch
? [
{
title: "Search",
shortcuts: [
{
keys: [modKey, "F"],
description: "Open search",
},
{
keys: ["Enter"],
description: "Next match",
},
{
keys: ["Shift", "Enter"],
description: "Previous match",
},
{
keys: ["Esc"],
description: "Close search",
},
],
},
]
: []),
{
title: "Filtering",
shortcuts: [
{
keys: [modKey, "Shift", "F"],
description: "Toggle the filter menu",
},
{
keys: ["Backspace"],
description: "Remove filter (when focused)",
},
{
keys: ["Delete"],
description: "Remove filter (when focused)",
},
],
},
{
title: "Sorting",
shortcuts: [
{
keys: [modKey, "Shift", "S"],
description: "Toggle the sort menu",
},
{
keys: ["Backspace"],
description: "Remove sort (when focused)",
},
{
keys: ["Delete"],
description: "Remove sort (when focused)",
},
],
},
{
title: "General",
shortcuts: [
{
keys: [modKey, "/"],
description: "Show keyboard shortcuts",
},
],
},
],
[
modKey,
enableSearch,
enableUndoRedo,
enablePaste,
enableRowAdd,
enableRowsDelete,
],
);
const filteredGroups = React.useMemo(() => {
if (!input.trim()) return shortcutGroups;
const query = input.toLowerCase();
return shortcutGroups
.map((group) => ({
...group,
shortcuts: group.shortcuts.filter(
(shortcut) =>
shortcut.description.toLowerCase().includes(query) ||
shortcut.keys.some((key) => key.toLowerCase().includes(query)),
),
}))
.filter((group) => group.shortcuts.length > 0);
}, [shortcutGroups, input]);
React.useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === SHORTCUT_KEY) {
event.preventDefault();
setOpen(true);
}
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, []);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
dir={dir}
className="max-w-2xl px-0"
initialFocus={() => {
inputRef.current?.focus();
return false;
}}
showCloseButton={false}
>
<DialogClose
className="absolute inset-e-6 top-6"
render={<Button variant="ghost" size="icon" className="size-6" />}
>
<IconPlaceholder
lucide="XIcon"
tabler="IconX"
hugeicons="Cancel01Icon"
phosphor="XIcon"
remixicon="RiCloseLine"
/>
</DialogClose>
<DialogHeader className="px-6">
<DialogTitle>Keyboard shortcuts</DialogTitle>
<DialogDescription className="sr-only">
Use these keyboard shortcuts to navigate and interact with the data
grid more efficiently.
</DialogDescription>
</DialogHeader>
<div className="px-6">
<div className="relative">
<IconPlaceholder
lucide="SearchIcon"
tabler="IconSearch"
hugeicons="Search01Icon"
phosphor="MagnifyingGlassIcon"
remixicon="RiSearchLine"
className="absolute inset-s-3 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground"
/>
<Input
ref={inputRef}
placeholder="Search shortcuts..."
className="ps-8"
value={input}
onChange={onInputChange}
/>
</div>
</div>
<Separator className="mx-auto data-horizontal:w-[calc(100%-(--spacing(12)))]" />
<div className="h-[40vh] overflow-y-auto px-6">
{filteredGroups.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center gap-3 text-center">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground">
<IconPlaceholder
lucide="SearchIcon"
tabler="IconSearch"
hugeicons="Search01Icon"
phosphor="MagnifyingGlassIcon"
remixicon="RiSearchLine"
className="pointer-events-none size-6"
/>
</div>
<div className="flex flex-col gap-1">
<div className="text-lg font-medium tracking-tight">
No shortcuts found
</div>
<p className="text-sm text-muted-foreground">
Try searching for a different term.
</p>
</div>
</div>
) : (
<div className="flex flex-col gap-6">
{filteredGroups.map((shortcutGroup) => (
<div key={shortcutGroup.title} className="flex flex-col gap-2">
<h3 className="text-sm font-semibold text-foreground">
{shortcutGroup.title}
</h3>
<div className="divide-y divide-border rounded-md border">
{shortcutGroup.shortcuts.map((shortcut, index) => (
<ShortcutCard
key={index}
keys={shortcut.keys}
description={shortcut.description}
/>
))}
</div>
</div>
))}
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
function ShortcutCard({
keys,
description,
}: ShortcutGroup["shortcuts"][number]) {
return (
<div className="flex items-center gap-4 px-3 py-2">
<span className="flex-1 text-sm">{description}</span>
<KbdGroup className="shrink-0">
{keys.map((key, index) => (
<React.Fragment key={key}>
{index > 0 && (
<span className="text-xs text-muted-foreground">+</span>
)}
<Kbd>{key}</Kbd>
</React.Fragment>
))}
</KbdGroup>
</div>
);
}
src/registry/bases/base/ui/direction.tsx
"use client";
export {
DirectionProvider,
useDirection,
} from "@base-ui/react/direction-provider";