component

Kbd

A keycap for displaying keyboard input — a single key or modifier rendered as a small, tactile <kbd> element. Kbd is built on top of Panel (surfaces, borders, radius) so its cap inherits the same theming as the rest of the system, and it locks to the control sizing scale so a key lines up beside same-size badges and buttons. Each Kbd renders one key; compose several side by side to express a shortcut.

Size

Three sizes lock to the control scale (shifted one tier down, like Badge): sm is a compact inline key, md (default) is the everyday cap, and lg suits command palettes and prominent shortcut callouts. A single-character key reads as a balanced square; multi-character labels expand horizontally.

EscEscEsc
---
// GENERATED from packages/demos/kbd/size.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Kbd size="sm">Esc</Kbd>
  <Kbd size="md">Esc</Kbd>
  <Kbd size="lg">Esc</Kbd>
</div>

Emphasis

The emphasis prop (forwarded to Panel) controls the cap’s visual weight. tertiary (default) is a neutral keycap; secondary tints the cap with the tone ramp; primary fills it with a solid accent surface.

⌘⌘⌘
---
// GENERATED from packages/demos/kbd/emphasis.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Kbd emphasis="tertiary">⌘</Kbd>
  <Kbd emphasis="secondary">⌘</Kbd>
  <Kbd emphasis="primary">⌘</Kbd>
</div>

Tone Colors

Kbd inherits Panel’s semantic tone colors — pair tone with emphasis="secondary" to tint a cap for status or grouping.

N P S W D
---
// GENERATED from packages/demos/kbd/tone.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Kbd tone="neutral">N</Kbd>
  <Kbd tone="primary" emphasis="secondary">
    P
  </Kbd>
  <Kbd tone="success" emphasis="secondary">
    S
  </Kbd>
  <Kbd tone="warning" emphasis="secondary">
    W
  </Kbd>
  <Kbd tone="danger" emphasis="secondary">
    D
  </Kbd>
</div>

Key Combinations

Kbd is intentionally a single key. For a whole shortcut, reach for KbdGroup below — it parses the shortcut, picks platform labels and renders a cap per key. To lay out caps by hand instead, place several side by side with your own separator, or fuse them into a joined cluster with a Group.

⌘+K
⌃+⇧+P
---
// GENERATED from packages/demos/kbd/combo.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import { stack, flex } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const sep = css({ color: "neutral.text.muted", fontSize: "sm" });
---

<div
  class={stack({
    gap: "lg",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <div class={flex({ gap: "2xs", align: "center" })}>
    <Kbd>⌘</Kbd>
    <span class={sep}>+</span>
    <Kbd>K</Kbd>
  </div>

  <div class={flex({ gap: "2xs", align: "center" })}>
    <Kbd>⌃</Kbd>
    <span class={sep}>+</span>
    <Kbd>⇧</Kbd>
    <span class={sep}>+</span>
    <Kbd>P</Kbd>
  </div>
</div>

Customizing

Kbd forwards Panel’s surface props (background, border, radius, shadow, …) and exposes a passThrough bag for per-slot style and attribute overrides.

⏎ Tab Space
---
// GENERATED from packages/demos/kbd/custom.demo.tsx — edit the source, not this file.
import Kbd from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "md",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <Kbd radius="full">⏎</Kbd>
  <Kbd background="transparent" border="bold">
    Tab
  </Kbd>
  <Kbd
    passThrough={{
      root: {
        style: { letterSpacing: "wide", textTransform: "uppercase" },
        props: { "aria-label": "Space bar" },
      },
    }}
  >
    Space
  </Kbd>
</div>

KbdGroup

KbdGroup renders a whole shortcut — one or more chords, each a list of keys — from a single keys prop. By default each key is a quiet soft square — a bare <kbd>, no cap component; the keycap appearances (framed, separate) render every key as a real Kbd, so those caps theme and size exactly like standalone ones. It ships from the same package as Kbd (import { KbdGroup } from "@pindoba/svelte-kbd", and likewise for Astro, React, Vue and Solid), and it is what every shortcut in ListBox, Menu and Command Palette renders through.

keys takes a string, an array of keys (one chord) or an array of chords:

InputParsed
"⇧⌘P"one chord: ⇧ ⌘ P (modifier glyphs ⌘ ⇧ ⌥ ⌃ split apart)
"Ctrl+Shift+P", "Ctrl + Shift + P"one chord: Ctrl Shift P
"mod+K"one chord: the platform’s primary modifier + K
"⌘K Z"a sequence: ⌘K, then Z (whitespace separates chords)
"Ctrl++"Ctrl and the plus key
["mod", "Shift", "P"]one chord, each entry one key (never split)
[["G"], ["I"]]a sequence of two chords

The parser is exported from the core as a pure function, parseShortcut(input), next to toAriaKeyShortcuts(input, platform) — the same helper that fills a ListBox row’s aria-keyshortcuts.

Appearance

appearance="soft" (default) sets each key as a quiet square on a subtle fill — muted text, no border, no frame — so a shortcut hint reads at a glance without competing with the row it sits in. It is built for the shortcut column of ListBox, Menu and Command Palette rows. The other appearances are options:

  • framed holds one compact Kbd cap per key inside a single rounded frame, the same height as a same-size Kbd.
  • separate lays real Kbd caps out loose with a gap.
  • plain drops the keys’ surface for text — each key still its own element, with a clear gap between keys.
Shift+Command+PShift+Command+PShift+Command+PShift+Command+P
---
// GENERATED from packages/demos/kbd/group-appearance.demo.tsx — edit the source, not this file.
import { KbdGroup } from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "lg",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <KbdGroup keys="⇧⌘P" platform="mac" />
  <KbdGroup keys="⇧⌘P" platform="mac" appearance="framed" />
  <KbdGroup keys="⇧⌘P" platform="mac" appearance="separate" />
  <KbdGroup keys="⇧⌘P" platform="mac" appearance="plain" />
</div>

Size

size (sm, md default, lg) matches Kbd’s scale for every appearance. Inside a sized control row — a ListBox option, a Menu item, an Input affix — the group sizes itself from the row’s --affix-* vars instead, so it lines up with the row’s other addons.

Command+KCommand+KCommand+K
Command+KCommand+KCommand+K
Command+KCommand+KCommand+K
Command+KCommand+KCommand+K
---
// GENERATED from packages/demos/kbd/group-size.demo.tsx — edit the source, not this file.
import { KbdGroup } from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";

const appearances = ["soft", "framed", "separate", "plain"] as const;
const sizes = ["sm", "md", "lg"] as const;
---

<div class={stack({ gap: "md" })}>
  {appearances.map((appearance) => (
    <div
      class={stack({
        gap: "md",
        direction: "row",
        align: "center",
        flexWrap: "wrap",
      })}
    >
      {sizes.map((size) => (
        <KbdGroup
          keys="mod+K"
          platform="mac"
          size={size}
          appearance={appearance}
        />
      ))}
    </div>
  ))}
</div>

Sequences

Whitespace in a string (or an array of chords) makes a sequence of chords pressed one after another. The default sequenceSeparator="gap" separates them with space; sequenceSeparator="then" spells out a muted “then” (rename it with labels={{ then: "…" }}).

Command+K, then ZG, then H
Command+K, then ZCommand+K, then Z
---
// GENERATED from packages/demos/kbd/group-sequence.demo.tsx — edit the source, not this file.
import { KbdGroup } from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";

const row = stack({
  gap: "lg",
  direction: "row",
  align: "center",
  flexWrap: "wrap",
});
---

<div class={stack({ gap: "md" })}>
  <div class={row}>
    <KbdGroup keys="⌘K Z" platform="mac" />
    <KbdGroup keys={[["G"], ["H"]]} />
  </div>
  <div class={row}>
    <KbdGroup keys="⌘K Z" platform="mac" sequenceSeparator="then" />
    <KbdGroup
      keys="⌘K Z"
      platform="mac"
      appearance="plain"
      sequenceSeparator="then"
    />
  </div>
</div>

Key separator

separator="plus" draws a muted + between the keys of a chord; the default "none" relies on the keys’ own fill or caps (or, for plain, the gap) to separate them.

Control+Shift+PControl+Shift+PControl+Shift+P
---
// GENERATED from packages/demos/kbd/group-separator.demo.tsx — edit the source, not this file.
import { KbdGroup } from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
---

<div
  class={stack({
    gap: "lg",
    direction: "row",
    align: "center",
    flexWrap: "wrap",
  })}
>
  <KbdGroup keys="Ctrl+Shift+P" platform="other" separator="plus" />
  <KbdGroup
    keys="Ctrl+Shift+P"
    platform="other"
    separator="plus"
    appearance="separate"
  />
  <KbdGroup
    keys="Ctrl+Shift+P"
    platform="other"
    separator="plus"
    appearance="plain"
  />
</div>

Platform

Write mod for the platform’s primary modifier: it shows ⌘ on a Mac and Ctrl elsewhere. Off a Mac the other modifier glyphs read as names too (⌥ → Alt, ⇧ → Shift, ⌃ → Ctrl, ↵ → Enter). An explicit ⌘/Cmd is the Meta key and shows Meta off a Mac, so prefer mod for cross-platform shortcuts.

platform="auto" (default) renders the Mac labels on the server and on the first client render — so hydration always matches — then detects the viewer’s platform after mount (navigator.userAgentData.platform, then navigator.platform, then the user agent — an iPhone or iPad counts as Mac) and relabels. Svelte, React, Vue and Solid detect in a mount effect; Astro relabels on the Kbd group runtime (kbdGroupHydration from @pindoba/astro-kbd/runtime). Pass platform="mac" or platform="other" to pin the labels.

macCommand+Shift+PCommand+Option+F
otherControl+Shift+PControl+Alt+F
autoCommand+Shift+PCommand+Option+F
---
// GENERATED from packages/demos/kbd/group-platform.demo.tsx — edit the source, not this file.
import { KbdGroup } from "@pindoba/astro-kbd";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";

const label = css({
  color: "neutral.text.muted",
  fontSize: "sm",
  minWidth: "5ch",
});
const row = stack({
  gap: "md",
  direction: "row",
  align: "center",
  flexWrap: "wrap",
});
---

<div class={stack({ gap: "md" })}>
  <div class={row}>
    <span class={label}>mac</span>
    <KbdGroup keys="mod+Shift+P" platform="mac" />
    <KbdGroup keys="mod+Alt+F" platform="mac" />
  </div>
  <div class={row}>
    <span class={label}>other</span>
    <KbdGroup keys="mod+Shift+P" platform="other" />
    <KbdGroup keys="mod+Alt+F" platform="other" />
  </div>
  <div class={row}>
    <span class={label}>auto</span>
    <KbdGroup keys="mod+Shift+P" />
    <KbdGroup keys="mod+Alt+F" />
  </div>
</div>

Accessibility

KbdGroup uses the HTML-spec markup for key combinations: an outer <kbd> containing a <kbd> per key. The visible keys, separators and “then” text are aria-hidden; a visually hidden label reads the shortcut as words (“Command+Shift+P”, “Command+K, then Z”). Set decorative when the shortcut is already announced elsewhere — ListBox, Menu and Command Palette rows do this and expose the shortcut through the row’s aria-keyshortcuts instead. aria-keyshortcuts cannot express a sequence, so multi-chord shortcuts leave it unset.

Customizing

The cap surface props (emphasis, tone, background, border, radius, shadow, translucent) are forwarded to every cap under the keycap appearances (framed, separate) — soft and plain keys are bare <kbd>s with no cap to style, so reach them through passThrough.key.style; frameBackground, frameBorder and frameRadius style the framed outer frame — it has no border ring by default (frameBorder="none"); pass frameBorder="muted" for an outlined pill. passThrough reaches the root, chord, key, separator, then and srLabel slots.

  • Components
  • Blocks