Kbd
A keyboard key or chord, used in tooltips, menus and shortcut hints.
⌘↵to add
import { Button } from "@oration/canon/components/button";import { Kbd, KbdGroup } from "@oration/canon/components/kbd";import { Textarea } from "@oration/canon/components/textarea";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function Hero() { const id = React.useId(); const [note, setNote] = React.useState( "Confirmed the new remit-to address with Aisha Bello at Northwind Freight.", ); const [mac, setMac] = React.useState(true); React.useEffect(() => { setMac( /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent), ); }, []); const send = () => { if (!note.trim()) return; toast.add({ type: "success", title: "Note added to INV-20417" }); setNote(""); }; return ( <div className="flex w-full max-w-md flex-col gap-2 rounded-xl bg-card p-3 shadow-border"> <label htmlFor={id} className="sr-only"> Internal note </label> <Textarea id={id} value={note} onChange={(event) => setNote(event.target.value)} onKeyDown={(event) => { if ( event.key === "Enter" && (event.metaKey || event.ctrlKey) ) { event.preventDefault(); send(); } }} placeholder="Add an internal note" /> <div className="flex items-center justify-between gap-2"> <p className="flex items-center gap-1.5 text-xs text-muted-foreground max-md:hidden"> <KbdGroup> <Kbd>{mac ? "⌘" : "Ctrl"}</Kbd> <Kbd>↵</Kbd> </KbdGroup> to add </p> <Button type="button" size="sm" className="ml-auto" onClick={send} aria-keyshortcuts={mac ? "Meta+Enter" : "Control+Enter"} > Add note </Button> </div> </div> );}Usage#
Kbd draws one keyboard key: a 20px Well Gray keycap with 12px Slate Meta text in Geist Sans. It teaches shortcuts where they are secondary to the action, in tooltips, inline hints beside keyboard-driven lists, and on the button a shortcut triggers. KbdGroup lines up the keys of a chord. The key is only the visual half: the shortcut itself still has to exist in code, and the control it triggers should declare it with aria-keyshortcuts.
When to use
- After an action's name in a tooltip: Search suppliers ⌘ K.
- In a one-line hint beside a keyboard-driven list: J K to move, A to approve, S to skip.
- On the button a shortcut triggers, such as Save ⌘ S in the save bar or Add note ⌘ ↵ under a composer.
- In help and onboarding copy that names a key: Press Esc to close the preview.
When not to use
- For a count or a qualifier on a control. Use Badge
- For a shortcut column inside a menu, which has its own right-aligned shortcut slot. Use Dropdown menu
- For machine strings such as IDs, keys or codes. Those are inline code in Geist Mono. Use Copy row
- As the only way to learn what a control does. The action's name comes first; the keys follow it. Use Tooltip
The Machine Mono Rule
font-sans is built into Kbd), like figures, labels and headings.The Thirteen-Fourteen Rule
Anatomy#
- Group.
KbdGroup, an inline flex row with 4px gaps that holds the keys of one chord. - Modifier key. A Kbd holding a modifier symbol (⌘ ⇧ ⌥ ⌃) or word (Ctrl, Shift, Alt). Modifiers come first.
- Key. A Kbd: 20px tall, at least 20px wide, 4px side padding, 6px corners, Well Gray fill, 12px Slate Meta text at weight 500. Icons inside are 12px.
Examples#
Keys and chords
One Kbd per key. Named keys are spelled out, letter keys are capitals as printed on the keycap, and a chord is a KbdGroup with the modifiers first. Show ⌘ on Mac and Ctrl elsewhere.
import { Kbd, KbdGroup } from "@oration/canon/components/kbd";export function KeysAndChords() { return ( <div className="grid grid-cols-2 gap-x-10 gap-y-4 text-13 sm:grid-cols-4"> <div className="flex flex-col items-start gap-2"> <Kbd>Esc</Kbd> <span className="text-xs text-muted-foreground">Named key</span> </div> <div className="flex flex-col items-start gap-2"> <Kbd>J</Kbd> <span className="text-xs text-muted-foreground"> Letter key </span> </div> <div className="flex flex-col items-start gap-2"> <KbdGroup> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </KbdGroup> <span className="text-xs text-muted-foreground"> Chord on Mac </span> </div> <div className="flex flex-col items-start gap-2"> <KbdGroup> <Kbd>Ctrl</Kbd> <Kbd>K</Kbd> </KbdGroup> <span className="text-xs text-muted-foreground"> Chord elsewhere </span> </div> </div> );}In a tooltip
The most common home for a shortcut: after the action's name. Inside a tooltip the key inverts to a 20% white fill on Graphite Ink by itself, and the tooltip's right padding tightens to 6px.
import { Button } from "@oration/canon/components/button";import { Kbd, KbdGroup } from "@oration/canon/components/kbd";import { toast } from "@oration/canon/components/toast";import { Tooltip, TooltipContent, TooltipTrigger } from "@oration/canon/components/tooltip";import { SearchIcon } from "lucide-react";import * as React from "react";export function InTooltip() { const [mac, setMac] = React.useState(true); React.useEffect(() => { setMac( /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent), ); }, []); return ( <Tooltip> <TooltipTrigger render={ <Button type="button" variant="outline" size="icon" aria-label="Search suppliers" onClick={() => toast.add({ title: "Search suppliers", description: "Opens the supplier search.", }) } /> } > <SearchIcon aria-hidden="true" /> </TooltipTrigger> <TooltipContent> Search suppliers <KbdGroup> <Kbd>{mac ? "⌘" : "Ctrl"}</Kbd> <Kbd>K</Kbd> </KbdGroup> </TooltipContent> </Tooltip> );}Inline hint
A single line of Caption text that teaches the keys of a keyboard-driven list, beside its heading. It hides below 768px, where there is usually no keyboard.
Needs your call12
JKto move,Ato approve,Sto skip
import { Kbd } from "@oration/canon/components/kbd";export function InlineHint() { return ( <div className="flex w-full max-w-lg items-baseline justify-between gap-4"> <h3 className="text-sm font-semibold text-foreground"> Needs your call <span className="ml-2 font-normal text-muted-foreground tabular-nums"> 12 </span> </h3> <p className="hidden items-center gap-1.5 text-xs text-muted-foreground md:flex"> <Kbd>J</Kbd> <Kbd>K</Kbd> to move, <Kbd>A</Kbd> to approve, <Kbd>S</Kbd> to skip </p> </div> );}On a filled button
On the indigo fill, restate the colors as 15% Indigo Paper with Indigo Paper text. Hide the key from screen readers and put the shortcut on the button as aria-keyshortcuts.
import { Button } from "@oration/canon/components/button";import { Kbd, KbdGroup } from "@oration/canon/components/kbd";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function OnPrimary() { const save = React.useCallback(() => { toast.add({ type: "success", title: "Wrap-up saved", description: "Call with Halcyon Logistics, disposition Resolved.", }); }, []); React.useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { const target = event.target as HTMLElement | null; if (event.defaultPrevented || target?.closest("input, textarea")) return; if (event.key === "Enter" && event.metaKey) { event.preventDefault(); save(); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [save]); return ( <Button type="button" aria-keyshortcuts="Meta+Enter" onClick={save}> Save wrap-up <KbdGroup aria-hidden="true" className="ml-0.5"> <Kbd className="bg-primary-foreground/15 text-primary-foreground"> ⌘ </Kbd> <Kbd className="bg-primary-foreground/15 text-primary-foreground"> ↵ </Kbd> </KbdGroup> </Button> );}States#
import { Button } from "@oration/canon/components/button";import { Kbd, KbdGroup } from "@oration/canon/components/kbd";export function Contexts() { return ( <div className="grid w-full grid-cols-2 gap-6 sm:grid-cols-4"> <div className="flex flex-col gap-3"> <span className="text-xs text-muted-foreground"> On the plane </span> <div className="flex h-9 items-center"> <KbdGroup> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </KbdGroup> </div> </div> <div className="flex flex-col gap-3"> <span className="text-xs text-muted-foreground">In a well</span> <div className="flex h-9 items-center rounded-[10px] bg-muted/70 px-3"> <KbdGroup> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </KbdGroup> </div> </div> <div className="flex flex-col gap-3"> <span className="text-xs text-muted-foreground"> In a tooltip </span> <div className="flex h-9 items-center"> <div data-slot="tooltip-content" className="inline-flex items-center gap-1.5 rounded-md bg-foreground py-1.5 pr-1.5 pl-3 text-xs text-background" > Search <KbdGroup> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </KbdGroup> </div> </div> </div> <div className="flex flex-col gap-3"> <span className="text-xs text-muted-foreground"> On a filled button </span> <div className="flex h-9 items-center"> <Button type="button" tabIndex={-1} className="pointer-events-none" > Save <KbdGroup aria-hidden="true" className="ml-0.5"> <Kbd className="bg-primary-foreground/15 text-primary-foreground"> ⌘ </Kbd> <Kbd className="bg-primary-foreground/15 text-primary-foreground"> S </Kbd> </KbdGroup> </Button> </div> </div> </div> );}| State | Treatment |
|---|---|
| Rest | The only state. Kbd is not interactive: pointer-events-none and select-none keep it out of clicks and text selection. |
| In a tooltip | Inside data-slot="tooltip-content", the fill becomes the background at 20% (10% in dark) and the text turns background-colored, so the key reads on Graphite Ink. |
| On a filled button | Not automatic. Pass bg-primary-foreground/15 text-primary-foreground so the key reads on Quiet Indigo. |
Behavior#
- Kbd renders a native
<kbd data-slot="kbd">; KbdGroup renders a<kbd data-slot="kbd-group">, the HTML way to nest keys of one input. - Kbd draws the key and nothing else. Register the shortcut yourself, skip it while focus is in a text field unless it uses a modifier, and don't take shortcuts the browser or the page already owns.
- Detect the platform after mount (
navigator.platformor the user agent) and show ⌘ on Mac and Ctrl elsewhere. Render the Mac glyphs first to avoid a flash on the most common platform. - Tooltips pad 6px on the right when they contain a key, so the keycap sits close to the tooltip's edge.
- Hint rows are hidden below 768px (
max-md:hiddenorhidden md:flex), where most people have no keyboard.
Do and don't#
Press G then I to open invoices
Press G then I to open invoices
Content#
- Letter keys are capitals, as printed on the keycap: J, K, not j.
- Named keys are spelled out with a capital first letter: Esc, Tab, Enter, Space. On Mac, Return is ↵.
- Mac modifiers are symbols in Apple's order: ⌃ ⌥ ⇧ ⌘. Elsewhere use words: Ctrl, Alt, Shift.
- Hints say what the key does with a verb: to approve, to skip, separated by commas, not dots.
- Sequences read in words: G then I, not G I or G, I.
Accessibility#
- Screen readers read key glyphs inconsistently (⌘ may be read as command or as a symbol name). Put the shortcut on the control it triggers with
aria-keyshortcuts, such as"Meta+Enter"or"Control+S", and mark a key inside a buttonaria-hidden="true". - Only declare
aria-keyshortcutsfor a shortcut that actually works on the page. - Tooltips aren't reliably announced, so a shortcut shown only in a tooltip isn't discoverable by screen reader users.
aria-keyshortcutson the trigger closes the gap. - Single-letter shortcuts must not fire while focus is in a text field, and should be possible to turn off (WCAG 2.1.4).
- Slate Meta on Well Gray measures about 5.4:1 in light and 6.2:1 in dark in this page's previews. Don't lighten key text further or drop it onto a darker well.
Design tokens#
| Token | Used for |
|---|---|
--muted | Keycap fill |
--muted-foreground | Key text |
--background | Key fill at 20% (10% in dark) and key text inside tooltips |
--primary-foreground | Manual override on filled buttons: fill at 15% and text |
--radius-sm | 6px keycap corners |
text-xs | 12px key text at weight 500 |
font-sans | Geist Sans, forced so keys never inherit mono |
API reference#
Kbd
One key.
Other props spread onto <kbd>.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | No default | The key's legend: a letter, a word such as Esc, a symbol such as ⌘, or a 12px icon. |
className | string | No default | Merged last. Use it for the on-primary override or a margin. |
KbdGroup
The keys of one chord, 4px apart.
Other props spread onto <kbd> (typed as <div> props).
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | No default | Kbd elements, modifiers first. |
className | string | No default | Merged last. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
KbdGroup is typed as React.ComponentProps<"div"> but renders <kbd>, so its ref and event types describe a div.
Kbd adapts to tooltips but not to filled buttons. The save bar, the contact center offer card and the wrap-up panel each restate bg-primary-foreground/15 text-primary-foreground by hand.
Call sites disagree on chords: the save bar and wrap-up panel put ⌘S and ⌘↵ in one key, while the ticket composer uses a KbdGroup with one key each. This page recommends one key per Kbd.
There is no shared platform helper. The save bar has its own useIsMac, and other call sites show ⌘ to everyone.