Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Kbd

Keyboard keys and key combinations.

Installation

pnpm dlx zazz-ui add kbd
npx zazz-ui add kbd
yarn dlx zazz-ui add kbd
bunx zazz-ui add kbd

Run 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

⌘ K ⌘ ⇧ P ↑ ↓ to navigate
<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

kbd attributes
AttributeKindValuesNotes
data-ui="kbd-group"Identityfree valueIdentity token in data-ui.
<ui-kbd-group>Tag formfree valueTag 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.

kbd hooks
HookDefaultState
--ui-kbd-bgvar(--color-muted)—
--ui-kbd-border1px solid transparent—
--ui-kbd-font-familyvar(--font-family-mono)—
--ui-kbd-font-sizevar(--font-size-xs)—
--ui-kbd-gapvar(--spacing)—
--ui-kbd-group-gapvar(--spacing)—
--ui-kbd-icon-sizecalc(var(--spacing) * 3)—
--ui-kbd-leading1—
--ui-kbd-min-hcalc(var(--spacing) * 4.5)—
--ui-kbd-min-wcalc(var(--spacing) * 4.5)—
--ui-kbd-pxvar(--spacing)—
--ui-kbd-py0—
--ui-kbd-roundedvar(--radius-xs)—
--ui-kbd-textvar(--color-muted-foreground)—