Shadcn Keyboard Table for React and Tailwind
Key / Description rows with keycap shortcuts.
| Key | Description |
|---|---|
Tab | Next focusable element. |
Shift+Tab | Previous focusable element. |
ArrowDown | Next trigger. |
ArrowUp | Previous trigger. |
Installation
bunx --bun shadcn@latest add https://kit.dev/r/keyboard-table.jsonpnpm dlx shadcn@latest add https://kit.dev/r/keyboard-table.jsonnpx shadcn@latest add https://kit.dev/r/keyboard-table.jsonyarn shadcn@latest add https://kit.dev/r/keyboard-table.jsonThis component depends on Table and Kbd. Install those first if you haven't already.
Install the following dependencies:
bun add @ark-ui/reactpnpm add @ark-ui/reactnpm install @ark-ui/reactyarn add @ark-ui/reactCopy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import type React from "react";
import { createContext, useContext } from "react";
import { cn } from "@/lib/utils";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
type KbdVariant = "default" | "outline";
interface KeyboardTableContextValue {
kbdVariant: KbdVariant;
}
const KeyboardTableContext = createContext<KeyboardTableContextValue>({
kbdVariant: "outline",
});
const KEY_COMBO_RE = /\s*\+\s*/;
const keyColumnClassName = "w-[11rem] min-w-fit max-w-[46%]";
const parseShortcutKeys = (keys: string | string[]) => {
if (Array.isArray(keys)) {
return keys.map((key) => key.trim()).filter((key) => key !== "");
}
return keys.split(KEY_COMBO_RE).filter((key) => key !== "");
};
export interface KeyboardTableProps
extends React.ComponentProps<typeof ark.div> {
/**
* Kbd variant for every row. Outline reads as keycaps.
*
* @default "outline"
*/
kbdVariant?: KbdVariant;
/**
* Row density.
*
* @default "default"
*/
size?: "default" | "compact";
}
export interface KeyboardTableItemProps
extends Omit<React.ComponentProps<typeof TableRow>, "children"> {
/** Description of the shortcut. */
children?: React.ReactNode;
/**
* Shortcut. A string is split on `+` (`"Shift + Tab"` → Shift, Tab).
* Pass an array to skip splitting.
*/
keys: string | string[];
}
const headerCellClassName = cn(
"h-auto bg-muted/50 px-3 py-1.5 font-medium text-xs",
"group-data-[size=compact]/keyboard-table:py-1"
);
const KeyboardTableHeader = () => (
<TableHeader data-slot="keyboard-table-header">
<TableRow>
<TableHead
className={cn(headerCellClassName, keyColumnClassName, "rounded-s-lg")}
>
Key
</TableHead>
<TableHead className={cn(headerCellClassName, "rounded-e-lg")}>
Description
</TableHead>
</TableRow>
</TableHeader>
);
const shortcutPartKey = (key: string, used: Map<string, number>) => {
const n = used.get(key) ?? 0;
used.set(key, n + 1);
return n === 0 ? key : `${key}-${n}`;
};
const ShortcutKeys = (props: { keys: string[]; variant: KbdVariant }) => {
const { keys, variant } = props;
const used = new Map<string, number>();
const parts: React.ReactNode[] = [];
for (const key of keys) {
const id = shortcutPartKey(key, used);
if (parts.length > 0) {
parts.push(
<span
aria-hidden="true"
className="select-none font-normal text-[0.6875rem] text-muted-foreground"
key={`plus:${id}`}
>
+
</span>
);
}
parts.push(
<Kbd className="h-6 min-w-6 px-1.5" key={`kbd:${id}`} variant={variant}>
{key}
</Kbd>
);
}
return (
<KbdGroup aria-label={keys.join(" + ")} dir="ltr">
{parts}
</KbdGroup>
);
};
export const KeyboardTable = (props: KeyboardTableProps) => {
const {
kbdVariant = "outline",
size = "default",
className,
children,
...rest
} = props;
return (
<KeyboardTableContext.Provider value={{ kbdVariant }}>
<ark.div
className={cn(
"group/keyboard-table not-prose",
"@container flex w-full min-w-0 flex-col overflow-hidden rounded-xl border bg-card p-1 text-card-foreground text-sm",
className
)}
data-size={size}
data-slot="keyboard-table"
{...rest}
>
<Table aria-label="Keyboard shortcuts" isHoverable={false}>
<KeyboardTableHeader />
<TableBody data-slot="keyboard-table-body">{children}</TableBody>
</Table>
</ark.div>
</KeyboardTableContext.Provider>
);
};
export const KeyboardTableItem = (props: KeyboardTableItemProps) => {
const { keys, children, className, ...rest } = props;
const { kbdVariant } = useContext(KeyboardTableContext);
const parsedKeys = parseShortcutKeys(keys);
return (
<TableRow
className={cn("last:border-b-0", className)}
data-slot="keyboard-table-item"
{...rest}
>
<TableCell
className={cn(
keyColumnClassName,
"whitespace-nowrap px-3 py-2",
"group-data-[size=compact]/keyboard-table:py-1.5"
)}
>
<ShortcutKeys keys={parsedKeys} variant={kbdVariant} />
</TableCell>
<TableCell
className={cn(
"min-w-0 whitespace-normal text-pretty px-3 py-2",
"group-data-[size=compact]/keyboard-table:py-1.5"
)}
>
{children}
</TableCell>
</TableRow>
);
};Update the import paths to match your project setup.
Anatomy
KeyboardTable
└── KeyboardTableItemshadcn.io Keyboard Table is a framed Table. KeyboardTable renders the table and a Key / Description header (th). Each KeyboardTableItem is one tr. The Key column renders Kbd keycaps (outline by default) with a muted + between combo parts.
The outer card matches Type Table: rounded-xl border bg-card p-1. Hover on rows is off (isHoverable={false}). Rows are not expandable.
Usage
import {
KeyboardTable,
KeyboardTableItem,
} from "@/components/ui/keyboard-table";<KeyboardTable>
<KeyboardTableItem keys="Space">
When focus is on a collapsed trigger, expand the row.
</KeyboardTableItem>
<KeyboardTableItem keys="Shift + Tab">
Move focus to the previous focusable element.
</KeyboardTableItem>
</KeyboardTable>A string keys is split on +. Pass a string[] when you do not want splitting — glyphs like ⌘ belong in an array.
Examples
Combo
Pass an array for modifier glyphs, or a string with + for named keys.
Muted keys
kbdVariant="default" uses the filled Kbd style instead of outline keycaps.
Compact
size="compact" reduces row padding. Keycaps stay the same size.
Guides
Keys
keys="Shift + Tab" becomes two keycaps. Extra spaces around + are ignored. Use keys={["⌘", "K"]} for symbols so a + inside a glyph is not a separator.
Each key is a <kbd>. The group has dir="ltr" so combos stay left-to-right in RTL layouts, and aria-label is the joined shortcut (Shift + Tab).
Keyboard Table vs Table vs Data List vs Type Table
| Keyboard Table | Table | Data List | Type Table | |
|---|---|---|---|---|
| Shape | Two-column shortcut table | Rows and columns | Label / value pairs | Expandable Prop / Type rows |
| Use when | Keyboard support, shortcut lists | Tabular comparison | Metadata, summaries | Component API docs |
Keyboard Table is Table with a Key / Description header and keycaps. Use Table when you need extra columns, selection, or TanStack Table.
For a single shortcut on a Button or Tooltip, use Kbd directly.
API Reference
shadcn.io Keyboard Table wraps Table and Kbd. Defaults below are shadcn.io values. kbdVariant is "outline" here ("default" on Kbd). Row hover is off.
asChild merges props onto a single child element.
KeyboardTable
Root. Renders a div around a Table (table + Key / Description header).
| Attribute | Description |
|---|---|
data-slot | keyboard-table |
data-size | "default" or "compact" |
Root styles: @container, rounded-xl border bg-card p-1 text-card-foreground. The header is data-slot="keyboard-table-header". The body is data-slot="keyboard-table-body". The inner table has aria-label="Keyboard shortcuts".
KeyboardTableItem
One shortcut row. Renders a Table row (tr) with Key and Description cells.
| Attribute | Description |
|---|---|
data-slot | keyboard-table-item |
Accessibility
Uses native table semantics. Key and Description are column headers (th). Each shortcut is a row; the first cell is the keycaps, the second is the description. Keycaps are native kbd elements.
- Decorative
+separators arearia-hidden. The grouparia-labelis the readable combo. - Do not put a second interactive control in the Key column. Links and buttons belong in the description.
dir="ltr"on the key group keeps modifier order stable in RTL.
Keyboard support
The table is not a composite widget. Links and buttons inside descriptions participate in the normal tab order.