I shipped the perfect keyboard shortcut hook

3min read1views

I am a keyboard-centered user, and in pretty much every project I build I rely on shortcuts to move faster.

Writing shortcut logic is not hard. Writing shortcut logic that stays readable after a few months is the hard part.

A lot of libs pushed me toward string parsing or config objects. I wanted syntax that reads like intent.

So I built @remcostoeten/use-shortcut: a React hook with a fluent, chainable API.

quick start

1
2
3
bun add @remcostoeten/use-shortcut
# or: npm i @remcostoeten/use-shortcut
# or: pnpm add @remcostoeten/use-shortcut
1
2
3
4
5
6
7
8
9
10
11
import { useShortcut } from '@remcostoeten/use-shortcut'

function Editor() {
	const $ = useShortcut()

	$.mod.key('s').on(saveDocument)
	$.mod.key('k').on(openSearch)
	$.shift.key('/').on(toggleHelp)

	return null
}

That is basically the whole idea: short, readable chains.

try it live

You can test the shortcuts below directly in this post:

Try the syntax and test the shortcuts in one place.

Save (cross-platform)
$.mod.key('s').on(saveDocument)
Search with typing guard
$.mod.key('k').except('typing').on(openSearch)
Help overlay
$.shift.key('/').on(toggleHelp)
Pause shortcuts
$.mod.key('p').on(togglePause)
ctrl+sctrl+kshift+/ctrl+p
Saves: 0
Searches: 0
Status: active shortcuts
No events yet.

why I built this

1) fluent, explicit syntax

I wanted shortcut code to be obvious at a glance.

1
2
3
4
5
// common pattern
useHotkeys('cmd+k', toggleSearch)

// this library
$.cmd.key('k').on(toggleSearch)

It is explicit about modifiers and key, and TypeScript guides you through each step.

2) cross-platform .mod

Command on Mac vs Control on Windows/Linux is always a pain.

.mod handles that automatically:

  • macOS -> cmd
  • Windows/Linux -> ctrl
1
2
3
4
$.mod.key('s').on(e => {
	e.preventDefault()
	saveDocument()
})

3) smart exceptions

Global shortcuts are great until users are typing in an input.

1
$.key('/').except('typing').on(focusSearch)

Built-in presets:

  • 'input' for input, textarea, select
  • 'editable' for contentEditable
  • 'typing' for input + editable
  • 'modal' when a modal/dialog is open
  • 'disabled' for disabled focused targets

You can also pass multiple presets or a custom predicate:

1
2
$.key('/').except(['input', 'modal']).on(handler)
$.key('k').except(e => e.target?.classList?.contains('no-shortcuts') === true).on(handler)

syntax reference

Modifiers

1
2
3
4
5
6
7
$.ctrl.key('s')
$.shift.key('enter')
$.alt.key('n')
$.cmd.key('k')
$.mod.key('k')
$.ctrl.shift.key('p')
$.cmd.shift.alt.key('a')

Supported keys

  • Letters: 'a' to 'z'
  • Numbers: '0' to '9'
  • Function keys: 'f1' to 'f12'
  • Navigation: 'up', 'down', 'left', 'right', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright', 'home', 'end', 'pageup', 'pagedown'
  • Special: 'enter', 'return', 'escape', 'esc', 'space', 'tab', 'backspace', 'delete', 'del', 'insert'
  • Symbols: 'minus', 'plus', 'equal', 'equals', 'bracketleft', 'bracketright', 'backslash', 'slash', '/', 'comma', 'period', 'semicolon', 'quote', 'backtick'

Handler APIs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.mod.key('s').on(save, {
	preventDefault: true,
	stopPropagation: false,
	delay: 100,
	disabled: false,
	description: 'Save doc',
	except: 'typing'
})

$.mod.key('k').handle({
	handler: openSearch,
	preventDefault: true,
	except: ['input', 'modal']
})

Return value from .on()

1
2
3
4
5
6
7
8
9
10
11
12
const save = $.mod.key('s').on(saveDocument)

save.display
save.combo
save.isEnabled
save.enable()
save.disable()
save.trigger()
save.unbind()
save.onAttempt?.((matched, event) => {
	// optional feedback/debug hook
})

useShortcut() options

1
2
3
4
5
6
7
8
const $ = useShortcut({
	debug: true,
	delay: 0,
	ignoreInputs: true,
	disabled: false,
	eventType: 'keydown', // or 'keyup'
	target: window
})

also works outside React

1
2
3
4
5
6
import { createShortcut } from '@remcostoeten/use-shortcut'

const $ = createShortcut()
const save = $.mod.key('s').on(saveDocument)

save.unbind()

final note

This package started as a DX itch: shortcuts should be easy to read and hard to mess up.

If that sounds like your thing, give it a spin:

Check it out on NPM

React:

Comments

Sign in to join the conversation

Loading sign-in options...