Skip to content

Kbd

ui

Preview

<div class=${kbdGroupClass()} role="img" aria-label="Command Shift P">
  <kbd class=${kbdClass()}>⌘</kbd>
  <kbd class=${kbdClass()}>Shift</kbd>
  <kbd class=${kbdClass()}>P</kbd>
</div>

Installation

webjs ui add kbd

API Reference

Parts

Name Description
kbdClass() Apply to a native <kbd>.
kbdGroupClass() Inline-flex wrapper for sequences like ⌘ + K.

Source: components/ui/kbd.ts

/**
 * Kbd: keyboard chord display. Tier-1 class helpers; compose with the
 * native `<kbd>` element for correct semantics.
 *
 * shadcn parity:
 *   Kbd       → kbdClass()
 *   KbdGroup  → kbdGroupClass()
 *
 * Design tokens used: --muted, --muted-foreground, --background.
 *
 * A11y (required for accessible output):
 *   SYMBOL-ONLY KEYS NEED A SPOKEN NAME. A screen reader reads a bare glyph
 *   however its speech engine happens to handle that codepoint, so a key shown
 *   as a symbol can be announced as a meaningless character name or skipped
 *   entirely. Spell the name the way a person says it: `⌘` is "Command", `⇧`
 *   "Shift", `⌥` "Option", `⌃` "Control", `⏎` "Enter", `⌫` "Backspace", `␣`
 *   "Space", `⎋` "Escape", and the arrows are "Up arrow" and friends. A key
 *   already spelled in words ("Shift", "K") reads correctly and needs nothing.
 *   DO NOT put `aria-label` on the `<kbd>` itself. `<kbd>` maps to
 *   `role=generic`, where a name is prohibited and browsers ignore it, so it
 *   would silently do nothing. Supply the name as TEXT instead: hide the glyph
 *   with `aria-hidden="true"` and put the spoken form in a visually-hidden
 *   sibling (`sr-only`), which every screen reader reads.
 *   NAME THE CHORD, not just the keys, since three `<kbd>` elements are
 *   otherwise announced as three separate things with no hint they are pressed
 *   together. Wrap the group in an element with `role="img"` and an
 *   `aria-label` of the whole chord: `role="img"` both SUPPORTS a name (unlike
 *   generic) and makes its children presentational, so the chord is announced
 *   once as one thing. Do not use `role="group"` + `aria-hidden` children for
 *   this: that leaves the group with nothing to announce.
 *   Keep the native `<kbd>` element. The class is styling only; `<kbd>` is what
 *   carries the "this is keyboard input" semantic.
 *   `<kbd>` DOCUMENTS a shortcut, it does not create one. If the shortcut is
 *   real, it also needs a keyboard handler, must not clash with an assistive-tech
 *   or browser binding, and per WCAG 2.1.4 a single-character shortcut has to be
 *   remappable or disableable.
 *
 * @example
 * ```html
 * <!-- A symbol key: hide the glyph, supply the spoken form as text. An
 *      aria-label on the <kbd> itself would be ignored (role=generic). -->
 * <kbd class=${kbdClass()} aria-hidden="true">⌘</kbd><span class="sr-only">Command</span>
 *
 * <!-- A spelled-out key needs nothing. -->
 * <kbd class=${kbdClass()}>K</kbd>
 *
 * <!-- A chord, announced once as one thing. role="img" supports a name AND
 *      makes its children presentational, so the glyphs are not read twice. -->
 * <div class=${kbdGroupClass()} role="img" aria-label="Command Shift P">
 *   <kbd class=${kbdClass()}>⌘</kbd>
 *   <kbd class=${kbdClass()}>Shift</kbd>
 *   <kbd class=${kbdClass()}>P</kbd>
 * </div>
 * ```
 */

export const kbdClass = (): string =>
  "pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none [&_svg:not([class*='size-'])]:size-3 [[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10";

export const kbdGroupClass = (): string => 'inline-flex items-center gap-1';