Skip to content

Components

keybinds provides four web components for discoverability and customization:

  • <command-palette> - Search-driven command execution (like VS Code's Ctrl+Shift+P)
  • <keybind-cheatsheet> - Glanceable reference (like ChatGPT's hold-Ctrl overlay)
  • <keybind-settings> - Rebindable keyboard shortcuts panel
  • <context-menu> - Right-click context menus driven by commands

Setup

js
import { registerComponents } from 'keybinds'

registerComponents()  // Defines all custom elements

Then use in HTML:

html
<command-palette></command-palette>
<keybind-cheatsheet></keybind-cheatsheet>
<keybind-settings></keybind-settings>
<context-menu></context-menu>

Command Palette

Search and execute commands by name.

html
<command-palette auto-trigger></command-palette>

Properties

PropertyTypeDescription
commandsCommand[]Array of command definitions
contextobjectContext for when checks
openbooleanShow/hide the palette

Attributes

AttributeDescription
openWhen present, palette is visible
auto-triggerEnable default $mod+K trigger
placeholderInput placeholder text (default: "Type a command...")

Events

EventDetailDescription
execute{ command }Fired when a command is executed
close-Fired when palette is dismissed

Usage

js
const palette = document.querySelector('command-palette')
palette.commands = commands
palette.context = { hasSelection: true }

// Manual trigger
button.onclick = () => palette.open = true

// Listen for execution
palette.addEventListener('execute', (e) => {
  console.log('Executed:', e.detail.command.id)
})

Keybind Cheatsheet

Grouped display of available bindings.

html
<keybind-cheatsheet auto-trigger></keybind-cheatsheet>

Properties

PropertyTypeDescription
commandsCommand[]Array of command definitions
contextobjectContext for when checks (inactive commands grayed)
openbooleanShow/hide the cheatsheet

Attributes

AttributeDescription
openWhen present, cheatsheet is visible
auto-triggerEnable hold-Control trigger (200ms delay)

Events

EventDescription
closeFired when cheatsheet is dismissed

Usage

js
const cheatsheet = document.querySelector('keybind-cheatsheet')
cheatsheet.commands = commands
cheatsheet.context = getContext()

// Manual trigger
helpButton.onclick = () => cheatsheet.open = true

Keybind Settings

Rebindable keyboard shortcuts panel with conflict detection.

html
<keybind-settings></keybind-settings>

Properties

PropertyTypeDescription
storeBindingsStoreReactive bindings store instance
openbooleanShow/hide the settings panel

Attributes

AttributeDescription
openWhen present, settings panel is visible

Events

EventDetailDescription
close-Fired when settings panel is dismissed
change{ commandId, keys?, mouse? }Fired when a binding is changed
reset{ commandId? }Fired when bindings are reset

Usage

js
import { BindingsStore, defineSchema, registerComponents } from 'keybinds'

const schema = defineSchema({
  save: { label: 'Save', category: 'File', keys: ['$mod+s'] },
  open: { label: 'Open', category: 'File', keys: ['$mod+o'] },
})

const store = new BindingsStore(schema, 'myapp:keybinds')
registerComponents()

const settings = document.querySelector('keybind-settings')
settings.store = store

// Open settings
settingsButton.onclick = () => settings.open = true

// Listen for changes
settings.addEventListener('change', (e) => {
  console.log('Changed:', e.detail.commandId)
})

Behavior

  • Groups commands by category
  • Click a binding to re-record it
  • Click "+ Key" or "+ Mouse" to add a new binding
  • Conflicts are detected and shown inline with Replace/Cancel options
  • Per-command and global reset buttons
  • Escape cancels recording or closes the panel

Context Menu

Right-click context menus populated from your commands.

html
<context-menu auto-trigger></context-menu>

Properties

PropertyTypeDescription
commandsCommand[]Array of command definitions
contextobjectContext for when checks
menustringFilter commands by menu tag
position{ x: number, y: number }Menu position in viewport coordinates
openbooleanShow/hide the menu

Attributes

AttributeDescription
openWhen present, menu is visible
auto-triggerListen for contextmenu events on the parent (or target)
menuFilter commands to those with a matching menu tag
targetCSS selector for the element to listen on (defaults to parent element)

Events

EventDetailDescription
execute{ command }Fired when a command is executed
close-Fired when the menu is dismissed

Usage

Commands opt into context menus via the menu property:

js
const commands = [
  {
    id: 'cut',
    label: 'Cut',
    category: 'Edit',
    keys: ['$mod+x'],
    menu: 'editor',
    execute: () => cut(),
  },
  {
    id: 'paste',
    label: 'Paste',
    category: 'Edit',
    keys: ['$mod+v'],
    menu: ['editor', 'sidebar'],  // Appears in multiple menus
    execute: () => paste(),
  },
]

Basic right-click menu on the parent element:

html
<div class="editor">
  <context-menu auto-trigger></context-menu>
</div>
js
const menu = document.querySelector('context-menu')
menu.commands = commands
menu.menu = 'editor'  // Only show commands tagged with 'editor'

Target a specific element instead of the parent:

html
<context-menu auto-trigger target="#canvas" menu="canvas"></context-menu>

Manual positioning:

js
const menu = document.querySelector('context-menu')
menu.commands = commands
menu.position = { x: event.clientX, y: event.clientY }
menu.open = true

Behavior

  • Items are grouped by category with separators between groups
  • Inactive commands (failing when) are shown but disabled
  • Hidden commands are excluded
  • The menu auto-clamps to viewport edges
  • Keyboard navigation with Arrow keys (wrap-around), Home/End, Enter to execute, Escape to close
  • Clicking the backdrop or right-clicking outside closes the menu

onModifierHold Utility

For custom hold-to-show behavior:

js
import { onModifierHold } from 'keybinds'

const cleanup = onModifierHold('Control', (held) => {
  cheatsheet.open = held
}, { delay: 200 })

// Later: cleanup()

Options

OptionTypeDefaultDescription
delaynumber200Milliseconds before triggering
targetEventTargetwindowElement to listen on

Multiple modifiers

js
onModifierHold(['Control', 'Meta'], callback)  // Either modifier