Skip to content

Kbd

A keyboard key or chord, used in tooltips, menus and shortcut hints.

Status
Stable
Level
Atom
Category
Content
Adoption
Not used yet
import { Kbd } from "@oration/canon/components/kbd";
packages/canon/src/components/kbd.tsx

⌘↵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

Geist Mono is only for strings a machine produced or will parse. Keyboard keys are read by people, so they stay in Geist Sans (font-sans is built into Kbd), like figures, labels and headings.

The Thirteen-Fourteen Rule

Keys are Label type, 12px at weight 500. Hints built from them are 12px Caption in Slate Meta, so they sit one step below the 13px dense text they explain.

Anatomy#

⇧⌘P
  1. Group. KbdGroup, an inline flex row with 4px gaps that holds the keys of one chord.
  2. Modifier key. A Kbd holding a modifier symbol (⌘ ⇧ ⌥ ⌃) or word (Ctrl, Shift, Alt). Modifiers come first.
  3. 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.

EscNamed key
JLetter key
⌘KChord on Mac
CtrlKChord 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

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#

On the plane
⌘K
In a well
⌘K
In a tooltip
Search⌘K
On a filled button
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>    );}
States
StateTreatment
RestThe only state. Kbd is not interactive: pointer-events-none and select-none keep it out of clicks and text selection.
In a tooltipInside 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 buttonNot 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.platform or 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:hidden or hidden md:flex), where most people have no keyboard.

Do and don't#

⇧⌘P
Do. Put each key in its own Kbd and group a chord with KbdGroup, modifiers first.
Cmd + Shift + P
Don't. Write a chord as one key with plus signs. It is wide, it doesn't look like the keyboard and it mixes words with symbols.

Press G then I to open invoices

Do. Keep keys in Geist Sans, the Kbd default.

Press G then I to open invoices

Don't. Set keys in Geist Mono. Keys aren't machine strings, and mono makes the hint look like code.
Approve invoiceA
Do. Name the action first and let the key follow it: Approve invoice A.
A
Don't. Show a tooltip with only a key. People who don't know the shortcut learn nothing about the action.

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 button aria-hidden="true".
  • Only declare aria-keyshortcuts for 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-keyshortcuts on 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#

Design tokens
TokenUsed for
--mutedKeycap fill
--muted-foregroundKey text
--backgroundKey fill at 20% (10% in dark) and key text inside tooltips
--primary-foregroundManual override on filled buttons: fill at 15% and text
--radius-sm6px keycap corners
text-xs12px key text at weight 500
font-sansGeist Sans, forced so keys never inherit mono

API reference#

Kbd

One key.

Other props spread onto <kbd>.

Props of Kbd
PropTypeDefaultDescription
childrenReactNodeNo defaultThe key's legend: a letter, a word such as Esc, a symbol such as ⌘, or a 12px icon.
classNamestringNo defaultMerged 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).

Props of KbdGroup
PropTypeDefaultDescription
childrenReactNodeNo defaultKbd elements, modifiers first.
classNamestringNo defaultMerged 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.