Shortcut Recorder

Records a key combination and renders it with platform symbols.

@drift-ui/shortcut-recorderRegistry JSON

Display only CtrlShiftP

On this page

Installation

The CLI also installs the npm packages and registry items it builds on. Register the namespace once first with https://drift-ui.dev/r/{style}/{name}.json. The CLI fills in {style} from your components.json and installs the Base UI or Radix build to match. See setup.

API reference

ShortcutRecorder

Click, then press a combination. The value is portable, such as "Mod+Shift+K", where Mod is Cmd on macOS and Ctrl elsewhere.

Prop
valuestringControlled shortcut.
defaultValuestring= ""Initial value when uncontrolled.
onValueChange(shortcut: string) => voidCalled with the recorded shortcut.
placeholderstring= "Record shortcut"Text while nothing is recorded.
requireModifierboolean= trueIgnore keys pressed without a modifier, except function keys.

Other props go to the <button>.

ShortcutKeys

Renders a stored shortcut as kbd elements for the current platform.

Prop
shortcutRequiredstringA shortcut string such as "Mod+K".

Other props go to the <span>.

matchesShortcut

matchesShortcut(event: KeyboardEvent, shortcut: string, isMac: boolean): boolean

Import from @/lib/shortcut. Returns true when a keyboard event matches a stored shortcut.

Source

components/ui/shortcut-recorder.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  isFunctionKey,
  isModifierKey,
  modifiersFromEvent,
  shortcutFromEvent,
  shortcutKeyLabel,
  splitShortcut,
  type ShortcutModifier,
} from "@/lib/shortcut"
import { useControllableState } from "@/hooks/use-controllable-state"

function subscribeToNothing() {
  return () => {}
}

function readIsMac() {
  return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
}

function useIsMac() {
  return React.useSyncExternalStore(subscribeToNothing, readIsMac, () => false)
}

function ShortcutKeys({
  shortcut,
  className,
  ...props
}: React.ComponentProps<"span"> & { shortcut: string }) {
  const isMac = useIsMac()

  return (
    <span
      data-slot="shortcut-keys"
      className={cn("inline-flex items-center gap-1", className)}
      {...props}
    >
      {splitShortcut(shortcut).map((part, index) => (
        <kbd
          key={`${part}-${index}`}
          data-slot="shortcut-key"
          className="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1 font-sans text-[11px] font-medium text-muted-foreground"
        >
          {shortcutKeyLabel(part, isMac)}
        </kbd>
      ))}
    </span>
  )
}

function ShortcutRecorder({
  value,
  defaultValue = "",
  onValueChange,
  placeholder = "Record shortcut",
  requireModifier = true,
  className,
  ...props
}: Omit<
  React.ComponentProps<"button">,
  "value" | "defaultValue" | "onChange"
> & {
  value?: string
  defaultValue?: string
  onValueChange?: (shortcut: string) => void
  placeholder?: string
  requireModifier?: boolean
}) {
  const isMac = useIsMac()
  const { value: shortcut, setValue: setShortcut } = useControllableState({
    value,
    defaultValue,
    onChange: onValueChange,
  })
  const [isRecording, setIsRecording] = React.useState(false)
  const [heldModifiers, setHeldModifiers] = React.useState<ShortcutModifier[]>(
    []
  )

  function stopRecording() {
    setIsRecording(false)
    setHeldModifiers([])
  }

  function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
    if (!isRecording) return
    const modifiers = modifiersFromEvent(event, isMac)
    const hasModifier = modifiers.length > 0
    const isLeavingWithTab =
      event.key === "Tab" && modifiers.every((modifier) => modifier === "Shift")
    if (isLeavingWithTab) return stopRecording()

    event.preventDefault()
    event.stopPropagation()
    if (isModifierKey(event.key)) return setHeldModifiers(modifiers)
    if (!hasModifier && event.key === "Escape") return stopRecording()
    if (!hasModifier && ["Backspace", "Delete"].includes(event.key)) {
      setShortcut("")
      return stopRecording()
    }
    if (requireModifier && !hasModifier && !isFunctionKey(event.key)) return

    setShortcut(shortcutFromEvent(event, isMac) ?? "")
    stopRecording()
  }

  function handleKeyUp(event: React.KeyboardEvent<HTMLButtonElement>) {
    if (!isRecording) return
    setHeldModifiers(modifiersFromEvent(event, isMac))
  }

  return (
    <button
      type="button"
      {...props}
      data-slot="shortcut-recorder"
      data-recording={isRecording || undefined}
      aria-pressed={isRecording}
      onClick={() => (isRecording ? stopRecording() : setIsRecording(true))}
      onKeyDown={handleKeyDown}
      onKeyUp={handleKeyUp}
      onBlur={stopRecording}
      className={cn(
        "inline-flex h-9 min-w-40 items-center justify-between gap-3 rounded-md border border-input bg-transparent px-3 text-sm shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
        "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
        "data-recording:border-ring data-recording:ring-[3px] data-recording:ring-ring/50",
        "disabled:pointer-events-none disabled:opacity-50",
        className
      )}
    >
      <ShortcutRecorderDisplay
        isRecording={isRecording}
        heldModifiers={heldModifiers}
        shortcut={shortcut}
        placeholder={placeholder}
      />
      <span aria-live="polite" className="sr-only">
        {isRecording &&
          "Recording. Press a key combination, Escape to cancel, Backspace to clear."}
      </span>
    </button>
  )
}

function ShortcutRecorderDisplay({
  isRecording,
  heldModifiers,
  shortcut,
  placeholder,
}: {
  isRecording: boolean
  heldModifiers: ShortcutModifier[]
  shortcut: string
  placeholder: string
}) {
  if (isRecording && heldModifiers.length > 0)
    return <ShortcutKeys shortcut={heldModifiers.join("+")} />
  if (isRecording)
    return (
      <span className="flex items-center gap-2 text-muted-foreground">
        <span className="size-1.5 animate-pulse rounded-full bg-destructive" />
        Press keys
      </span>
    )
  if (shortcut) return <ShortcutKeys shortcut={shortcut} />
  return <span className="text-muted-foreground">{placeholder}</span>
}

export { ShortcutKeys, ShortcutRecorder }