Browse docs
Browse docs
Show the key that does the thing.
import { Kbd } from '@dashforge/tw';
Press <Kbd>Esc</Kbd> to closeEverything on this page could be had from a styled <span>. What a span
cannot give you is the <kbd> element, which assistive technology and reader
modes treat as keyboard input. That is the whole reason the component exists,
and it is the first thing its test suite asserts.
<Kbd>Esc</Kbd> {/* md, the default */}
<Kbd size="sm">⌘</Kbd> {/* dense chrome */}
<Kbd size="lg">Enter</Kbd> {/* a shortcuts panel */}Three steps. md matches body text and is the default; sm sits inside dense
chrome like a toolbar or a menu row; lg is for a dedicated shortcuts panel
where the keys are the content rather than an aside.
Press Esc to close, Enter to confirm.
import { Kbd, Stack, Typography } from '@dashforge/tw';
<Stack gap={3}>
<Typography variant="body2">
Press <Kbd>Esc</Kbd> to close, <Kbd>Enter</Kbd> to confirm.
</Typography>
<Stack direction="row" gap={3} align="center" wrap>
<Kbd size="sm">Esc</Kbd>
<Kbd size="md">Esc</Kbd>
<Kbd size="lg">Esc</Kbd>
</Stack>
</Stack>There is no keys prop. A chord is two caps and whatever separator your design
calls for, and baking one in would decide ⌘ + K against ⌘K for every
consumer of the library.
no separator
+
⇧+
Pwith one
import { Kbd, Stack, Typography } from '@dashforge/tw';
// A chord is two caps and whatever separator your design calls for.
// There is no `keys` prop: it would decide `⌘ + K` against `⌘K`
// for every consumer.
<Stack direction="row" gap={1} align="center">
<Kbd>⌘</Kbd>
<Kbd>K</Kbd>
</Stack>
<Stack direction="row" gap={1} align="center">
<Kbd>⌘</Kbd>
<Typography variant="body2" color="muted">+</Typography>
<Kbd>⇧</Kbd>
<Typography variant="body2" color="muted">+</Typography>
<Kbd>P</Kbd>
</Stack>No intent colour, and no dark: variant anywhere in the recipe. neutral
auto-inverts through the preset's CSS variables, so the cap reads as a raised
surface in light and a recessed one in dark from the same class list. Adding a
dark: variant here would invert twice and break the dark theme, which is the
documented anti-pattern for this palette.
The cap carries both a background and a border, but they are not equal partners. Measured against the two surfaces a keycap realistically sits on:
| vs page | vs card | |
|---|---|---|
surface neutral-100 | 1.09 | 1.04 |
surface neutral-200 | 1.26 | 1.21 |
border neutral-300 | 1.48 | 1.42 |
border neutral-400 | 2.52 | 2.42 |
border neutral-500 | 4.74 | 4.54 |
A neutral surface one step off the page is around 1.1:1 by construction, and in
dark mode bg-neutral-100 resolves to exactly a card's colour, so a cap leaning
on its background disappears into the panel. The border is at neutral-500 for
that reason, and the surface stays as a faint lift.
min-w-* matches the height at every step, so a single glyph renders square and
a word like Enter grows horizontally only. That is what keeps ⌘ and Esc
looking like the same control rather than two different ones.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The key label. Usually one glyph or a short word (Esc, Tab). |
className | string | undefined | Escape hatch for the rendered <kbd>: id, title, data-*. |
size | KbdSize | 'md' | Cap size. |
sx | string | undefined | Utility classes appended to the variant chain. Resolved through tailwind-merge, so a consumer's class always wins over the recipe. |
<Kbd> has no slots. It renders a single <kbd> element, so there is no
anatomy to target separately. Use sx for utility overrides and className
for the escape hatches (id, title, data-*).
size is configurable once for the whole app through the component defaults
registry:
patchTheme({
components: {
Kbd: { defaults: { size: 'sm' } },
},
});An explicit prop still wins over the theme, which the test suite pins at both levels.