# useHeldKeys

> The keys someone is holding down right now, shared by every subscriber through a single set of window listeners.

Docs: https://hextaui.com/docs/use-held-keys
Markdown: https://hextaui.com/docs/use-held-keys.md

```tsx title="components/examples/use-held-keys/demo.tsx"
"use client"

import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { useHeldKeys } from "@/hooks/use-held-keys"

export function UseHeldKeysDemo() {
  const held = useHeldKeys(true)

  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex h-8 items-center">
        {held.size > 0 ? (
          <KbdGroup>
            {[...held].map((key) => (
              <Kbd key={key} keys={key} size="lg" />
            ))}
          </KbdGroup>
        ) : (
          <span className="text-sm text-muted-foreground">
            Hold down any keys
          </span>
        )}
      </div>
      <code className="font-mono text-xs text-muted-foreground">
        {JSON.stringify([...held])}
      </code>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/use-held-keys.json
```

This adds the hook and anything it depends on.

### Manual

Copy and paste the following code into your project.

```ts title="hooks/use-held-keys.ts"
import * as React from "react"

const modifierNames: Record<string, string> = {
  Meta: "meta",
  OS: "meta",
  Control: "ctrl",
  Alt: "alt",
  AltGraph: "alt",
  Shift: "shift",
}

const emptyHeld: ReadonlySet<string> = new Set()

let held: ReadonlySet<string> = emptyHeld
const subscribers = new Set<() => void>()

function physicalKey(event: KeyboardEvent) {
  const modifier = modifierNames[event.key]
  if (modifier) {
    return modifier
  }
  const letter = /^Key([A-Z])$/.exec(event.code)
  if (letter) {
    return letter[1].toLowerCase()
  }
  const digit = /^(?:Digit|Numpad)([0-9])$/.exec(event.code)
  if (digit) {
    return digit[1]
  }
  if (event.key === " ") {
    return "space"
  }
  return event.key.toLowerCase()
}

function publish(next: ReadonlySet<string>) {
  held = next
  for (const notify of subscribers) {
    notify()
  }
}

function onKeyDown(event: KeyboardEvent) {
  if (event.repeat) {
    return
  }
  const key = physicalKey(event)
  if (!held.has(key)) {
    publish(new Set([...held, key]))
  }
}

function onKeyUp(event: KeyboardEvent) {
  const key = physicalKey(event)
  if (key === "meta") {
    publish(
      new Set(
        [...held].filter(
          (name) =>
            name !== "meta" && Object.values(modifierNames).includes(name)
        )
      )
    )
    return
  }
  if (held.has(key)) {
    const next = new Set(held)
    next.delete(key)
    publish(next)
  }
}

function releaseAll() {
  if (held.size > 0) {
    publish(emptyHeld)
  }
}

function onVisibilityChange() {
  if (document.visibilityState === "hidden") {
    releaseAll()
  }
}

function subscribeHeld(notify: () => void) {
  subscribers.add(notify)
  if (subscribers.size === 1) {
    window.addEventListener("keydown", onKeyDown, { passive: true })
    window.addEventListener("keyup", onKeyUp, { passive: true })
    window.addEventListener("blur", releaseAll)
    document.addEventListener("visibilitychange", onVisibilityChange)
  }
  return () => {
    subscribers.delete(notify)
    if (subscribers.size === 0) {
      window.removeEventListener("keydown", onKeyDown)
      window.removeEventListener("keyup", onKeyUp)
      window.removeEventListener("blur", releaseAll)
      document.removeEventListener("visibilitychange", onVisibilityChange)
      held = emptyHeld
    }
  }
}

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

function getHeld() {
  return held
}

function getServerHeld() {
  return emptyHeld
}

function useHeldKeys(enabled: boolean) {
  return React.useSyncExternalStore(
    enabled ? subscribeHeld : subscribeNothing,
    enabled ? getHeld : getServerHeld,
    getServerHeld
  )
}

export { useHeldKeys }
```

Update the import paths to match your project setup.

## Usage

```tsx
import { useHeldKeys } from "@/hooks/use-held-keys"
```

```tsx
const held = useHeldKeys(true)
const showShortcutHints = held.has("meta") || held.has("ctrl")
```

Use it for anything that reacts to keys being held rather than pressed: keycaps that press down, shortcut hints that appear while you hold ⌘, or a modifier that switches a tool, like Alt to duplicate while dragging.

## How it works

Every component that calls the hook shares one store. The first subscriber adds passive `keydown` and `keyup` listeners to `window`, and the last one to unsubscribe removes them. A page with fifty listening keycaps still has one pair of listeners.

The listeners only read events. They never call `preventDefault`, and they sit on `window`, after React's own handlers, so typing in a field is never delayed or changed.

```bash
"meta" "ctrl" "alt" "shift"     modifiers, left and right alike
"a" … "z"  "0" … "9"            letters and digits, by physical key
"space" "enter" "escape" "tab"   named keys, lowercased
"arrowup" "arrowdown" "f1" …
```

- Letters and digits come from `event.code`, the physical key, so holding Option+K on a Mac still reports `k` rather than `˚`.
- Key repeat is ignored, and nothing re-renders while a key is held.
- macOS doesn't send `keyup` for other keys while ⌘ is down. When ⌘ is released, the store keeps only the modifiers still held, so letters can't get stuck.
- Everything is released when the window loses focus or the tab is hidden. A shortcut that switches apps leaves nothing held.

## Examples

### Keycaps that press

`<Kbd listen>` is built on this hook. Each keycap presses down while its key is held.

```tsx title="components/examples/use-held-keys/shortcut.tsx"
"use client"

import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { useHeldKeys } from "@/hooks/use-held-keys"
import { useIsApple } from "@/lib/hotkey"

export function UseHeldKeysShortcut() {
  const held = useHeldKeys(true)
  const apple = useIsApple()
  const modifier = apple ? "meta" : "ctrl"
  const ready = held.has(modifier) && held.has("shift")

  return (
    <div className="flex flex-col items-center gap-3 text-sm">
      <KbdGroup>
        <Kbd keys="mod" listen />
        <Kbd keys="shift" listen />
        <Kbd keys="p" listen />
      </KbdGroup>
      <p className="text-muted-foreground">
        {ready ? "Now press P" : "Hold the modifiers to see the hint"}
      </p>
    </div>
  )
}
```

## Good to know

- Pass `false` to stop listening. The hook then returns an empty set and adds no listeners, so it's cheap to call it unconditionally.
- The set is replaced only when a key goes down or up, so its identity works as a memo or effect dependency.
- For shortcuts that fire an action, use `matchesHotkey` from `Hotkey` in a keydown handler instead. Holding keys is for showing state, not for running commands.
- On the server, and before hydration, the set is empty.

## API reference

### useHeldKeys(enabled)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | – | Whether to listen. When false, nothing is attached. |

| Returns | Description |
| --- | --- |
| `ReadonlySet<string>` | The names of the keys held down right now. |

### Used by

`Kbd` and `KbdGroup` through their `listen` prop.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
