Kbd

Keyboard key indicator with platform-aware modifier expansion via the shortcut prop.

Report a bug

Preview

Switch between light and dark to inspect the embedded Storybook preview.

Installation

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/kbd.json
bash

Storybook

Explore all variants, controls, and accessibility checks in the interactive Storybook playground.

View in Storybook

Code

"use client";

import {
  type ComponentPropsWithoutRef,
  forwardRef,
  type ReactNode,
  useSyncExternalStore,
} from "react";

import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "../../lib/utils";

const kbdVariants = cva(
  "inline-flex select-none items-center justify-center rounded border border-border bg-muted font-mono font-medium text-foreground shadow-[0_1px_0_0_hsl(var(--border))] [&>svg]:h-3 [&>svg]:w-3",
  {
    defaultVariants: {
      size: "md",
    },
    variants: {
      size: {
        lg: "h-7 min-w-[1.75rem] px-2 text-sm",
        md: "h-5 min-w-[1.25rem] px-1.5 text-xs",
        sm: "h-4 min-w-[1rem] px-1 text-[10px]",
      },
    },
  },
);

const MAC_PLATFORM_PATTERN = /mac|iphone|ipad|ipod/i;

const MODIFIER_LABELS: Record<
  "alt" | "ctrl" | "meta" | "mod" | "shift",
  { mac: string; other: string }
> = {
  alt: { mac: "⌥", other: "Alt" },
  ctrl: { mac: "⌃", other: "Ctrl" },
  meta: { mac: "⌘", other: "Win" },
  mod: { mac: "⌘", other: "Ctrl" },
  shift: { mac: "⇧", other: "Shift" },
};

const SPECIAL_KEY_LABELS: Record<string, string> = {
  arrowdown: "↓",
  arrowleft: "←",
  arrowright: "→",
  arrowup: "↑",
  backspace: "⌫",
  delete: "⌦",
  enter: "↵",
  escape: "Esc",
  return: "↵",
  space: "Space",
  tab: "⇥",
};

const SHORTCUT_SEPARATOR = /\s*\+\s*/;

function isMacPlatform(): boolean {
  if (typeof navigator === "undefined") return false;
  return MAC_PLATFORM_PATTERN.test(navigator.userAgent);
}

function noopUnsubscribe(): void {
  return;
}

function subscribeNoop(): () => void {
  return noopUnsubscribe;
}

function getServerSnapshot(): boolean {
  return false;
}

function useIsMac(): boolean {
  return useSyncExternalStore(subscribeNoop, isMacPlatform, getServerSnapshot);
}

function isModifier(value: string): value is keyof typeof MODIFIER_LABELS {
  return Object.hasOwn(MODIFIER_LABELS, value);
}

function formatToken(token: string, mac: boolean): string {
  const lowered = token.toLowerCase();
  if (isModifier(lowered)) {
    const labels = MODIFIER_LABELS[lowered];
    return mac ? labels.mac : labels.other;
  }
  const special = SPECIAL_KEY_LABELS[lowered];
  if (special !== undefined) return special;
  return token.length === 1 ? token.toUpperCase() : token;
}

/**
 * Props for {@link Kbd}.
 *
 * @public
 */
export type KbdProps = {
  /** Optional explicit children. Takes precedence over `shortcut`. */
  children?: ReactNode;
  /**
   * Shortcut string in the form `"mod+k"` or `"ctrl+shift+p"`. The `mod`
   * token expands to `⌘` on Mac and `Ctrl` elsewhere.
   */
  shortcut?: string;
} & ComponentPropsWithoutRef<"kbd"> &
  VariantProps<typeof kbdVariants>;

/**
 * Keyboard key indicator. Renders a single key when used with `children`,
 * or expands a shortcut string with platform-aware modifiers when given a
 * `shortcut` prop.
 *
 * @example
 * ```tsx
 * <Kbd>Ctrl</Kbd> + <Kbd>K</Kbd>
 *
 * {/* Renders ⌘+K on Mac, Ctrl+K elsewhere *\/}
 * <Kbd shortcut="mod+k" />
 * ```
 *
 * @public
 */
export const Kbd = forwardRef<HTMLElement, KbdProps>(
  ({ children, className, shortcut, size, ...rest }, ref) => {
    const isMac = useIsMac();

    if (children !== undefined) {
      return (
        <kbd
          className={cn(kbdVariants({ size }), className)}
          ref={ref}
          {...rest}
        >
          {children}
        </kbd>
      );
    }

    if (shortcut) {
      const tokens = shortcut.split(SHORTCUT_SEPARATOR).filter(Boolean);
      const ariaLabel = tokens
        .map((token) => formatToken(token, isMac))
        .join(" + ");
      return (
        <span aria-label={ariaLabel} className="inline-flex items-center gap-1">
          {tokens.map((token, index) => (
            <kbd
              className={cn(kbdVariants({ size }), className)}
              key={`${token}-${index.toString()}`}
              ref={index === 0 ? ref : undefined}
              {...(index === 0 ? rest : {})}
            >
              {formatToken(token, isMac)}
            </kbd>
          ))}
        </span>
      );
    }

    return (
      <kbd
        className={cn(kbdVariants({ size }), className)}
        ref={ref}
        {...rest}
      />
    );
  },
);
Kbd.displayName = "Kbd";

export { kbdVariants };
typescript

Dependencies

  • @vllnt/ui@^0.2.1
  • class-variance-authority