FeaturesKeybindings
Previous

Keybindings Storage

How keybindings persistence works with settings-backed storage.

What It Does

Keybindings are stored through the settings system (keybindings key in user settings) and synchronized via TRPC. This page focuses on the UI/runtime components you should use with that storage layer.

When To Use

  • You want shortcuts persisted across sessions/devices.
  • You need a settings page where users can customize shortcuts.
  • You want runtime handlers to always read the latest persisted shortcut values.

Prerequisites

  • Keybindings server filter initialized.
  • Settings schema filter includes keybindings schema (provided by keybindings package filters).

Components To Use

Component / APIPurposeTypical place
KeybindingsProviderProvides keybinding model + storage to all keybinding hooks/components.Dashboard app root/provider tree
KeybindingsTableFull settings UI to view/edit/reset shortcuts.Settings page (/settings/keybindings)
useKeybindingBinds shortcut events to runtime handlers.Feature/client components handling actions
useShortcutReads effective shortcut (user override or default).Labels, tooltips, command menus
KeybindingDisplayDisplays shortcut for a model action (actionSlug).Buttons, menu items, help panels
ShortcutDisplayDisplays an already-known shortcut string.Tables/lists/custom rows
DatabaseKeybindingsStoragePersists keybindings in user settings through TRPC.Provider storage when not injected by filters
LocalStorageKeybindingsStorageLocal browser fallback storage.Local-only apps or quick prototypes

Storage Modes

Local Storage vs Database Storage

DimensionLocalStorageKeybindingsStorageDatabaseKeybindingsStorage
Persistence locationBrowser localStorageUser settings in database (keybindings setting)
ScopeCurrent browser onlyUser account (shared across sessions/devices)
Sync across devicesNoYes
Requires authenticated backendNoYes (TRPC + settings API)
Offline behaviorWorks while offline in same browserDepends on backend availability for writes
Best fitDemos, local tools, non-auth appsProduction dashboards and multi-device users

Selection Rules

  • Use DatabaseKeybindingsStorage for authenticated apps where shortcuts must follow the user account.
  • Use LocalStorageKeybindingsStorage for local-only usage, prototypes, or environments without backend persistence.
  • If storage is not passed to KeybindingsProvider, it defaults to LocalStorageKeybindingsStorage.

How To Use

In app filters, call:

  • useKeybindingsFilters() on client.
  • initKeybindingsServerFilters() on server.

These hooks/filters:

  • inject keybindings schema into settings schema filters,
  • add settings page UI for keybindings,
  • wire database-backed DatabaseKeybindingsStorage via clientTrpc.

No separate custom keybindings route is required in the default architecture.

Integration Patterns

Use filter-based integration by default (recommended).

This is the standard dashboard setup:

  • useKeybindingsFilters() injects KeybindingsProvider and DatabaseKeybindingsStorage.
  • It also injects a settings page using KeybindingsTable.

You usually only need to define the model and register handlers with useKeybinding.

Register runtime behavior in client components.

Feature runtime handler
'use client';
 
import { useKeybinding } from '@kit/keybindings/ui';
 
export function SearchShortcutHandler() {
  useKeybinding('search.command', () => {
    // open command menu
  });
 
  return null;
}

Display shortcuts in your UI.

Shortcut display in button/label
'use client';
 
import { KeybindingDisplay, useShortcut } from '@kit/keybindings/ui';
 
export function SearchButtonHelp() {
  const { formattedShortcut } = useShortcut('search.command');
 
  return (
    <div className="flex items-center gap-2">
      <span>Search</span>
      <KeybindingDisplay actionSlug="search.command" />
      <span className="text-muted-foreground text-xs">({formattedShortcut})</span>
    </div>
  );
}

Use manual provider wiring only for custom app composition.

Use this when you do not rely on useKeybindingsFilters() injection:

Manual provider wiring
'use client'; import { keybindingsRouter } from '@kit/keybindings/router/router'; import { DatabaseKeybindingsStorage } from '@kit/keybindings/storage/settings-storage/handler'; import { KeybindingsProvider } from '@kit/keybindings/ui'; import type { KeybindingsProviderProps } from '@kit/keybindings/ui'; import type { TrpcClientWithQuery } from '@creatorem/next-trpc/query-client'; export function AppKeybindingsProvider({ clientTrpc, keybindingsModel, children, }: { clientTrpc: TrpcClientWithQuery<typeof keybindingsRouter>; keybindingsModel: KeybindingsProviderProps<any>['model']; children: React.ReactNode; }) { const storage = new DatabaseKeybindingsStorage(clientTrpc); return ( <KeybindingsProvider model={keybindingsModel} storage={storage}> {children} </KeybindingsProvider> ); }

Use local-only storage explicitly:

Manual local storage provider
'use client'; import { LocalStorageKeybindingsStorage } from '@kit/keybindings/storage/local-storage'; import { KeybindingsProvider } from '@kit/keybindings/ui'; import type { KeybindingsProviderProps } from '@kit/keybindings/ui'; export function LocalKeybindingsProvider({ keybindingsModel, children, }: { keybindingsModel: KeybindingsProviderProps<any>['model']; children: React.ReactNode; }) { return ( <KeybindingsProvider model={keybindingsModel} storage={new LocalStorageKeybindingsStorage()} > {children} </KeybindingsProvider> ); }

Filter API

Persistence works because keybindings filters inject schema/UI/provider wiring into settings and dashboard provider composition.

FilterParametersReturnRegistered By (package file)Initialized In (app entrypoint)Environment
display_trpc_provider_child_in_dashboard{ clientTrpc?: TrpcClientWithQuery<Router<typeof appRouter>>; url?: (s: string) => string; keybindingsModel?: ReturnType<typeof parseKeybindingsConfig> }React.ReactNodekit/keybindings/src/filters/use-filters.tsxapps/dashboard/hooks/use-filters.ts (useKeybindingsFilters)client
get_settings_schema{}SettingsSchemakit/keybindings/src/filters/use-filters.tsxapps/dashboard/hooks/use-filters.ts (useKeybindingsFilters)client
get_settings_ui_config{ clientTrpc: TrpcClientWithQuery<Router<unknown>> }ReturnType<typeof parseUISettingConfig>kit/keybindings/src/filters/use-filters.tsxapps/dashboard/hooks/use-filters.ts (useKeybindingsFilters)client
server_get_settings_schema{}SettingsSchemakit/keybindings/src/filters/server-filters.tsapps/dashboard/lib/init-server-filters.ts (initKeybindingsServerFilters)server
display_keybinding{ actionSlug: keyof KeybindingActions }React.ReactNodekit/keybindings/src/filters/use-filters-with-ctx.tsxKeybindingsProvider context (injected by useKeybindingsFilters)client
get_shortcut{ actionSlug: keyof KeybindingActions }{ shortcut: string | null; formattedShortcut: string } | nullkit/keybindings/src/filters/use-filters-with-ctx.tsxKeybindingsProvider context (injected by useKeybindingsFilters)client

MCP Context

capability: keybindings_persistence entrypoints: - kit/keybindings/src/filters/server-filters.ts - kit/keybindings/src/filters/use-filters.tsx - kit/keybindings/src/filters/use-filters-with-ctx.tsx - apps/dashboard/hooks/use-filters.ts - apps/dashboard/lib/init-server-filters.ts - kit/keybindings/src/router/* inputs: - action_id - shortcut outputs: - persisted_keybinding_map constraints: - persistence uses settings infrastructure side_effects: - updates user settings storage

Agent Recipe

  1. Ensure keybindings filters are initialized.
  2. Verify settings schema contains keybindings key at runtime.
  3. Test update flow through keybindings settings UI.

Troubleshooting

  • If values reset after reload, confirm TRPC and settings update route wiring.
  • If keybindings page is missing, verify useKeybindingsFilters() is called.
  • If useKeybinding throws provider errors, ensure KeybindingsProvider is mounted above the component tree.

API Reference

KeybindingsStorage

PropTypeDefault
getUserKeybindings*
function
setUserKeybinding*
function
resetKeybinding*
function
resetAllKeybindings*
function

How is this guide?

Last updated on 3/27/2026