shadcn.io is not affiliated with official shadcn/ui
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 Kbd and Table. Install them 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.
Usage
import {
KeyboardTable,
KeyboardTableItem,
} from "@/components/ui/keyboard-table";<KeyboardTable className="w-full max-w-lg">
<KeyboardTableItem keys="Tab">Next focusable element.</KeyboardTableItem>
<KeyboardTableItem keys="Shift + Tab">
Previous focusable element.
</KeyboardTableItem>
<KeyboardTableItem keys="ArrowDown">Next trigger.</KeyboardTableItem>
<KeyboardTableItem keys="ArrowUp">Previous trigger.</KeyboardTableItem>
</KeyboardTable>Demos
Combo
Size Compact
Variant Default
API Reference
KeyboardTable
div
PropType
AttributeType
KeyboardTableItem
PropType
AttributeType