HextaUI

Hotkey

Analiza, etiqueta, anuncia y reconoce atajos de teclado, con ⌘ en plataformas Apple y Ctrl en el resto.

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

Añade la utilidad y todo aquello de lo que depende a tu proyecto.

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>
}

Escribe cada atajo una sola vez, como cadena, y usa esa misma cadena para mostrarlo, anunciarlo y compararlo. mod significa ⌘ en las plataformas Apple y Ctrl en todas las demás. Casi siempre es lo que quieres, ya que Ctrl+K en un Mac y ⌘K en Windows se sienten ambos fuera de lugar.

"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

Las teclas se separan con + y no distinguen mayúsculas de minúsculas. Las teclas con nombre usan el valor KeyboardEvent.key en minúsculas, como enter, tab, pageup o f5. Usa space para la barra espaciadora.

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

parseHotkey devuelve un array por acorde, con los alias resueltos y los modificadores ordenados según el orden de Apple: Control, Option, Shift, Command. Cada etiqueta construida a partir de él se lee en el orden en que la gente espera verla.

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"

Las plataformas Apple usan símbolos sin separador, como los muestran los menús. Windows y Linux usan palabras unidas por +. Los símbolos son difíciles de leer en voz alta, así que spokenKey da el nombre que un lector de pantalla debe anunciar. <Kbd keys> muestra el símbolo y pone el nombre hablado en texto oculto visualmente.

useIsApple() elige la plataforma. Devuelve true en el servidor y durante la hidratación, y luego la respuesta real, de modo que un visitante de Windows ve brevemente ⌘ antes de Ctrl en lugar de recibir un error de hidratación.

Escucha de atajos

matchesHotkey comprueba un evento keydown contra un atajo. Los modificadores deben coincidir exactamente, así que mod+b no se dispara con mod+shift+b.

"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>
  )
}
  • Las letras y los dígitos también coinciden por tecla física, así que alt+k funciona en un Mac, donde Option+K escribe ˚.
  • Las letras con Shift coinciden: shift+k coincide con la K que produce Shift.
  • Compara un solo acorde. Para secuencias como g i, rastrea tú mismo el acorde anterior.
  • Omite los atajos sin modificadores mientras el foco está en un campo de texto, para que escribir una letra nunca dispare un comando.
ExportaciónDescripción
parseHotkey(hotkey)string[][]: un array por acorde, con los alias resueltos y los modificadores ordenados.
formatHotkey(hotkey, apple)La etiqueta de un acorde, como ⇧⌘K o Shift+Ctrl+K.
keyLabel(key, apple)La etiqueta visible para el nombre de una tecla.
spokenKey(key, apple)El nombre que un lector de pantalla debe anunciar para una tecla.
matchesHotkey(event, hotkey)Si un KeyboardEvent coincide con un acorde, con modificadores exactos.
isApplePlatform()Lee navigator.platform. true en el servidor.
useIsApple()isApplePlatform como un hook seguro para la hidratación.

Kbd y Command.