API
Kbd
Keyboard keys and key combinations.
Installation
pnpm dlx zazz-ui add kbdnpx zazz-ui add kbdyarn dlx zazz-ui add kbdbunx zazz-ui add kbdRun zazz-ui init first if the project has no zazz/ folder yet.
Examples
Default
⌘
⇧
⌥
↵
esc
<div
style="
--display: flex;
--items: center;
--gap: 6;
"
>
<kbd>⌘</kbd>
<kbd>⇧</kbd>
<kbd>⌥</kbd>
<kbd>↵</kbd>
<kbd>esc</kbd>
</div>/**
* kbd.css: Kbd (native <kbd>) + Kbd group (ui-kbd-group | [data-ui~="kbd-group"])
*
* @layer variables, reset, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved)
* @uses text-box: vertical trim (patchy browser support)
* @tokens --ui-kbd-*, --ui-kbd-group-* (@layer variables)
* @consumedby button.css, tooltip.css (reassign --ui-kbd-* in scope), command.css (selects the group)
*
* The key itself styles the bare native <kbd> (no identity needed) so its rules
* live in @layer reset (browser-policy exception, like the switch redraw) and
* lose to component scopes. Hooks are declared on :root, not on the element,
* so an ancestor scope (a button, a tooltip) can reassign a --ui-kbd-* token
* and inheritance carries it down to the key. Each utility-backed declaration
* reads var(--_<utility>-resolved, var(--ui-kbd-<utility>)): no variants, no privates.
*/
@layer variables {
:root {
/* type */
--ui-kbd-font-family: var(--font-family-mono);
--ui-kbd-font-size: var(--font-size-xs);
--ui-kbd-leading: 1;
/* surface */
--ui-kbd-bg: var(--color-muted);
--ui-kbd-text: var(--color-muted-foreground);
--ui-kbd-border: 1px solid transparent;
--ui-kbd-rounded: var(--radius-xs);
/* metrics (dual-mode: a scale number or a length) */
--ui-kbd-px: var(--spacing);
--ui-kbd-py: 0;
--ui-kbd-min-w: calc(var(--spacing) * 5);
--ui-kbd-min-h: calc(var(--spacing) * 5);
--ui-kbd-icon-size: calc(var(--spacing) * 3.5);
--ui-kbd-gap: var(--spacing);
/* group: the one gap between adjacent keys, everywhere */
--ui-kbd-group-gap: var(--spacing);
}
}
@layer reset {
/* ===========================================================================
KBD — native <kbd> baseline
=========================================================================== */
:where(kbd) {
display: inline-flex;
align-items: center;
justify-content: center;
--_gap-len: var(--_gap-resolved, var(--ui-kbd-gap));
--_gap-num: var(--_gap-resolved, var(--ui-kbd-gap));
gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
font-family: var(--_font-family-resolved, var(--ui-kbd-font-family));
font-size: var(--_font-size-resolved, var(--ui-kbd-font-size));
line-height: var(--_leading-resolved, var(--ui-kbd-leading));
background-color: var(--_bg-resolved, var(--ui-kbd-bg));
color: var(--_text-resolved, var(--ui-kbd-text));
--_px-len: var(--_px-resolved, var(--ui-kbd-px));
--_px-num: var(--_px-resolved, var(--ui-kbd-px));
padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
--_py-len: var(--_py-resolved, var(--ui-kbd-py));
--_py-num: var(--_py-resolved, var(--ui-kbd-py));
padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));
border: var(--ui-kbd-border);
border-radius: var(--_rounded-resolved, var(--ui-kbd-rounded));
overflow: hidden;
--_min-w-len: var(--_min-w-resolved, var(--ui-kbd-min-w));
--_min-w-num: var(--_min-w-resolved, var(--ui-kbd-min-w));
min-inline-size: calc(var(--_min-w-len) + var(--_min-w-num) * var(--spacing));
--_min-h-len: var(--_min-h-resolved, var(--ui-kbd-min-h));
--_min-h-num: var(--_min-h-resolved, var(--ui-kbd-min-h));
min-block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: fit-content;
text-box: trim-both ex alphabetic;
}
:where(kbd > svg) {
inline-size: var(--ui-kbd-icon-size);
block-size: var(--ui-kbd-icon-size);
}
}
@layer ui.components {
/* ===========================================================================
KBD GROUP — a run of keys forming one shortcut ("⌘ K")
- Wrap adjacent <kbd> siblings so their spacing is the shared
--ui-kbd-group-gap token instead of whatever gap the host happens to have.
=========================================================================== */
:where(ui-kbd-group, [data-ui~="kbd-group"]) {
display: inline-flex;
align-items: center;
--_gap-len: var(--_gap-resolved, var(--ui-kbd-group-gap));
--_gap-num: var(--_gap-resolved, var(--ui-kbd-group-gap));
gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
}
}Group
<div
style="
--display: flex;
--items: center;
--gap: 6;
"
>
<ui-kbd-group>
<kbd>⌘</kbd>
<kbd>K</kbd>
</ui-kbd-group>
<ui-kbd-group>
<kbd>⌘</kbd>
<kbd>⇧</kbd>
<kbd>P</kbd>
</ui-kbd-group>
<span>
<ui-kbd-group>
<kbd>↑</kbd>
<kbd>↓</kbd>
</ui-kbd-group>
<span
style="
--font-size: var(--font-size-sm);
--text: var(--color-muted-foreground);
"
>
to navigate
</span>
</span>
</div>/**
* kbd.css: Kbd (native <kbd>) + Kbd group (ui-kbd-group | [data-ui~="kbd-group"])
*
* @layer variables, reset, components
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved)
* @uses text-box: vertical trim (patchy browser support)
* @tokens --ui-kbd-*, --ui-kbd-group-* (@layer variables)
* @consumedby button.css, tooltip.css (reassign --ui-kbd-* in scope), command.css (selects the group)
*
* The key itself styles the bare native <kbd> (no identity needed) so its rules
* live in @layer reset (browser-policy exception, like the switch redraw) and
* lose to component scopes. Hooks are declared on :root, not on the element,
* so an ancestor scope (a button, a tooltip) can reassign a --ui-kbd-* token
* and inheritance carries it down to the key. Each utility-backed declaration
* reads var(--_<utility>-resolved, var(--ui-kbd-<utility>)): no variants, no privates.
*/
@layer variables {
:root {
/* type */
--ui-kbd-font-family: var(--font-family-mono);
--ui-kbd-font-size: var(--font-size-xs);
--ui-kbd-leading: 1;
/* surface */
--ui-kbd-bg: var(--color-muted);
--ui-kbd-text: var(--color-muted-foreground);
--ui-kbd-border: 1px solid transparent;
--ui-kbd-rounded: var(--radius-xs);
/* metrics (dual-mode: a scale number or a length) */
--ui-kbd-px: var(--spacing);
--ui-kbd-py: 0;
--ui-kbd-min-w: calc(var(--spacing) * 5);
--ui-kbd-min-h: calc(var(--spacing) * 5);
--ui-kbd-icon-size: calc(var(--spacing) * 3.5);
--ui-kbd-gap: var(--spacing);
/* group: the one gap between adjacent keys, everywhere */
--ui-kbd-group-gap: var(--spacing);
}
}
@layer reset {
/* ===========================================================================
KBD — native <kbd> baseline
=========================================================================== */
:where(kbd) {
display: inline-flex;
align-items: center;
justify-content: center;
--_gap-len: var(--_gap-resolved, var(--ui-kbd-gap));
--_gap-num: var(--_gap-resolved, var(--ui-kbd-gap));
gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
font-family: var(--_font-family-resolved, var(--ui-kbd-font-family));
font-size: var(--_font-size-resolved, var(--ui-kbd-font-size));
line-height: var(--_leading-resolved, var(--ui-kbd-leading));
background-color: var(--_bg-resolved, var(--ui-kbd-bg));
color: var(--_text-resolved, var(--ui-kbd-text));
--_px-len: var(--_px-resolved, var(--ui-kbd-px));
--_px-num: var(--_px-resolved, var(--ui-kbd-px));
padding-inline: calc(var(--_px-len) + var(--_px-num) * var(--spacing));
--_py-len: var(--_py-resolved, var(--ui-kbd-py));
--_py-num: var(--_py-resolved, var(--ui-kbd-py));
padding-block: calc(var(--_py-len) + var(--_py-num) * var(--spacing));
border: var(--ui-kbd-border);
border-radius: var(--_rounded-resolved, var(--ui-kbd-rounded));
overflow: hidden;
--_min-w-len: var(--_min-w-resolved, var(--ui-kbd-min-w));
--_min-w-num: var(--_min-w-resolved, var(--ui-kbd-min-w));
min-inline-size: calc(var(--_min-w-len) + var(--_min-w-num) * var(--spacing));
--_min-h-len: var(--_min-h-resolved, var(--ui-kbd-min-h));
--_min-h-num: var(--_min-h-resolved, var(--ui-kbd-min-h));
min-block-size: calc(var(--_min-h-len) + var(--_min-h-num) * var(--spacing));
inline-size: fit-content;
text-box: trim-both ex alphabetic;
}
:where(kbd > svg) {
inline-size: var(--ui-kbd-icon-size);
block-size: var(--ui-kbd-icon-size);
}
}
@layer ui.components {
/* ===========================================================================
KBD GROUP — a run of keys forming one shortcut ("⌘ K")
- Wrap adjacent <kbd> siblings so their spacing is the shared
--ui-kbd-group-gap token instead of whatever gap the host happens to have.
=========================================================================== */
:where(ui-kbd-group, [data-ui~="kbd-group"]) {
display: inline-flex;
align-items: center;
--_gap-len: var(--_gap-resolved, var(--ui-kbd-group-gap));
--_gap-num: var(--_gap-resolved, var(--ui-kbd-group-gap));
gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
}
}Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-ui="kbd-group" | Identity | free value | Identity token in data-ui. |
<ui-kbd-group> | Tag form | free value | Tag form; styled and, when scripted, registered under this name. |
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-kbd-bg | var(--color-muted) | — |
--ui-kbd-border | 1px solid transparent | — |
--ui-kbd-font-family | var(--font-family-mono) | — |
--ui-kbd-font-size | var(--font-size-xs) | — |
--ui-kbd-gap | var(--spacing) | — |
--ui-kbd-group-gap | var(--spacing) | — |
--ui-kbd-icon-size | calc(var(--spacing) * 3) | — |
--ui-kbd-leading | 1 | — |
--ui-kbd-min-h | calc(var(--spacing) * 4.5) | — |
--ui-kbd-min-w | calc(var(--spacing) * 4.5) | — |
--ui-kbd-px | var(--spacing) | — |
--ui-kbd-py | 0 | — |
--ui-kbd-rounded | var(--radius-xs) | — |
--ui-kbd-text | var(--color-muted-foreground) | — |