HextaUI

Hotkey

Tastenkürzel parsen, beschriften, ansagen und abgleichen, mit ⌘ auf Apple-Plattformen und Strg überall sonst.

Apple
⇧⌘K
Windows and Linux
Shift+Ctrl+K
Screen readers
Shift Command K
parseHotkey
[["shift","mod","k"]]

Click outside the field and press Shift Command Kmatched 0×

"use client"

import * as React from "react"

import { Input } from "@/components/ui/input"
import { Kbd } from "@/components/ui/kbd"
import {
  formatHotkey,
  matchesHotkey,
  parseHotkey,
  spokenKey,
  useIsApple,
} from "@/lib/hotkey"

export function HotkeyDemo() {
  const [hotkey, setHotkey] = React.useState("mod+shift+k")
  const [pressed, setPressed] = React.useState(0)
  const apple = useIsApple()
  const chord = parseHotkey(hotkey)[0] ?? []

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (
        event.target instanceof HTMLInputElement ||
        !matchesHotkey(event, hotkey)
      ) {
        return
      }
      event.preventDefault()
      setPressed((count) => count + 1)
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [hotkey])

  const rows = [
    ["Apple", formatHotkey(hotkey, true)],
    ["Windows and Linux", formatHotkey(hotkey, false)],
    ["Screen readers", chord.map((key) => spokenKey(key, apple)).join(" ")],
    ["parseHotkey", JSON.stringify(parseHotkey(hotkey))],
  ]

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Input
        aria-label="Hotkey"
        value={hotkey}
        onChange={(event) => setHotkey(event.target.value)}
        spellCheck={false}
        autoCapitalize="off"
      />
      <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-6 gap-y-2 text-sm">
        {rows.map(([label, value]) => (
          <React.Fragment key={label}>
            <dt className="text-muted-foreground">{label}</dt>
            <dd className="truncate font-mono">{value}</dd>
          </React.Fragment>
        ))}
      </dl>
      <p className="flex items-center gap-2 text-sm text-muted-foreground">
        Click outside the field and press <Kbd keys={hotkey} />
        <span className="ms-auto font-mono tabular-nums">
          matched {pressed}×
        </span>
      </p>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/hotkey.json

Fügt das Utility und alles, wovon es abhängt, zu deinem Projekt hinzu.

import {
  formatHotkey,
  matchesHotkey,
  parseHotkey,
  useIsApple,
} from "@/lib/hotkey"
function SaveShortcut({ onSave }: { onSave: () => void }) {
  const apple = useIsApple()

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if (matchesHotkey(event, "mod+s")) {
        event.preventDefault()
        onSave()
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [onSave])

  return <span>Save {formatHotkey("mod+s", apple)}</span>
}

Schreibe jedes Kürzel einmal als String und verwende denselben String, um es anzuzeigen, anzusagen und abzugleichen. mod bedeutet ⌘ auf Apple-Plattformen und Strg überall sonst. Das ist fast immer, was du willst, da sich Strg+K auf einem Mac und ⌘K unter Windows beide falsch anfühlen.

"mod+k"            ⌘K on Apple platforms, Ctrl+K elsewhere
"shift+mod+p"      modifiers in any order
"cmd+option+esc"   aliases: cmd, command, option, opt, control, esc, return
"alt+up"           up, down, left and right for the arrow keys
"mod++"            a literal plus key
"g g"              a sequence of two chords, separated by a space

Tasten werden durch + getrennt und unterscheiden nicht zwischen Groß- und Kleinschreibung. Benannte Tasten verwenden den kleingeschriebenen KeyboardEvent.key-Wert, etwa enter, tab, pageup oder f5. Verwende space für die Leertaste.

parseHotkey("mod+shift+k")  // [["shift", "mod", "k"]]
parseHotkey("cmd+opt+esc")  // [["alt", "meta", "escape"]]
parseHotkey("g i")          // [["g"], ["i"]]

parseHotkey gibt pro Akkord ein Array zurück, mit aufgelösten Aliassen und in Apples Reihenfolge sortierten Modifiern: Control, Option, Shift, Command. Jedes daraus gebaute Label liest sich in der Reihenfolge, in der man es zu sehen erwartet.

formatHotkey("mod+shift+k", true)   // "⇧⌘K"
formatHotkey("mod+shift+k", false)  // "Shift+Ctrl+K"
keyLabel("enter", true)             // "↵"
spokenKey("mod", true)              // "Command"
spokenKey("mod", false)             // "Control"

Apple-Plattformen verwenden Symbole ohne Trennzeichen, so wie Menüs sie zeigen. Windows und Linux verwenden Wörter, die mit + verbunden sind. Symbole sind schwer vorzulesen, daher liefert spokenKey den Namen, den ein Screenreader stattdessen ansagen soll. <Kbd keys> zeigt das Symbol und legt den gesprochenen Namen in visuell verborgenen Text.

useIsApple() wählt die Plattform. Es gibt auf dem Server und während der Hydration true zurück, dann die echte Antwort, sodass ein Windows-Besucher kurz ⌘ vor Strg sieht, statt einen Hydration-Fehler zu bekommen.

Kürzel-Listener

matchesHotkey prüft ein keydown-Event gegen einen Hotkey. Modifier müssen exakt passen, sodass mod+b nicht für mod+shift+b auslöst.

"use client"

import * as React from "react"

import { Kbd } from "@/components/ui/kbd"
import { matchesHotkey } from "@/lib/hotkey"

const shortcuts = [
  { hotkey: "mod+b", label: "Bold" },
  { hotkey: "mod+i", label: "Italic" },
  { hotkey: "mod+shift+x", label: "Strikethrough" },
]

export function HotkeyListener() {
  const [last, setLast] = React.useState<string>()

  React.useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      const match = shortcuts.find((shortcut) =>
        matchesHotkey(event, shortcut.hotkey)
      )
      if (match) {
        event.preventDefault()
        setLast(match.label)
      }
    }
    window.addEventListener("keydown", onKeyDown)
    return () => window.removeEventListener("keydown", onKeyDown)
  }, [])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <ul className="flex flex-col gap-2 text-sm">
        {shortcuts.map((shortcut) => (
          <li
            key={shortcut.hotkey}
            data-active={last === shortcut.label ? "" : undefined}
            className="flex items-center justify-between rounded-md px-2 py-1 transition-colors duration-150 data-active:bg-muted"
          >
            {shortcut.label}
            <Kbd keys={shortcut.hotkey} />
          </li>
        ))}
      </ul>
      <p role="status" className="text-sm text-muted-foreground">
        {last ? `Matched ${last}` : "Press a shortcut"}
      </p>
    </div>
  )
}
  • Buchstaben und Ziffern werden auch über die physische Taste abgeglichen, sodass alt+k auf einem Mac funktioniert, wo Option+K ein ˚ tippt.
  • Umgeschaltete Buchstaben passen: shift+k passt auf das K, das Shift erzeugt.
  • Es gleicht einen Akkord ab. Für Sequenzen wie g i verfolge den vorherigen Akkord selbst.
  • Überspringt Kürzel ohne Modifier, solange der Fokus in einem Textfeld liegt, sodass das Tippen eines Buchstabens nie einen Befehl auslöst.
ExportBeschreibung
parseHotkey(hotkey)string[][]: ein Array pro Akkord, mit aufgelösten Aliassen und sortierten Modifiern.
formatHotkey(hotkey, apple)Das Label für einen Akkord, etwa ⇧⌘K oder Shift+Ctrl+K.
keyLabel(key, apple)Das sichtbare Label für einen Tastennamen.
spokenKey(key, apple)Der Name, den ein Screenreader für eine Taste ansagen soll.
matchesHotkey(event, hotkey)Ob ein KeyboardEvent zu einem Akkord passt, mit exakten Modifiern.
isApplePlatform()Liest navigator.platform. Auf dem Server true.
useIsApple()isApplePlatform als hydration-sicherer Hook.

Kbd und Command.