VOOZH about

URL: https://dev.to/ignace/declarative-hotkeys-in-ember-with-tanstackhotkeys-3f95

⇱ Declarative Hotkeys in Ember with @tanstack/hotkeys - DEV Community


Building a modern Ember "Polaris" app means reaching for lean, type-safe tools that don't come with legacy baggage. Since we’re all-in on .gts and Vite, we need a hotkey solution that feels native to TypeScript and the Glimmer lifecycle.

The move is @tanstack/hotkeys. It’s headless, tiny, and does exactly what it says on the tin.


1. The Setup

pnpm add @tanstack/hotkeys

2. The Helper (on-hotkey.ts)

In a .gts world, we bridge the library to the component lifecycle using a class-based helper. Unlike a modifier, this helper doesn't need to be attached to a specific element—you just invoke it at the root of your template.

This setup handles the registration and teardown automatically: when the helper enters the template, the key is registered; when the template is destroyed, the listener is killed.

import Helper from '@ember/component/helper';
import type { RegisterableHotkey, HotkeyOptions } from '@tanstack/hotkeys';
import { HotkeyManager } from '@tanstack/hotkeys';

interface Signature {
 Args: {
 Positional: [hotkey: RegisterableHotkey, callback: () => void];
 Named: HotkeyOptions;
 };
 Return: void;
}

export default class OnHotkeyHelper extends Helper<Signature> {
 private unregister?: () => void;

 compute(
 [hotkey, callback]: [RegisterableHotkey, () => void],
 options: HotkeyOptions,
 ) {
 this.unregister?.();

 const manager = HotkeyManager.getInstance();
 const handle = manager.register(hotkey, () => callback(), options);

 this.unregister = () => handle.unregister();
 }

 willDestroy() {
 super.willDestroy();
 this.unregister?.();
 }
}

3. Usage in .gts

You invoke the helper at the top level of your template. It’s declarative: if the component is rendered, the hotkey is active.

One of the best parts of @tanstack/hotkeys is the formatForDisplay utility. It handles the annoying logic of showing ⌘K to Mac users and Ctrl+K to everyone else automatically.

import onHotkey from './helpers/on-hotkey';
import { formatForDisplay } from '@tanstack/hotkeys';

const combo = 'Mod+k';

<template>
 {{! Invoke at the root: no element needed }}
 {{onHotkey combo @onOpen}}

 <div class="search-trigger">
 <button type="button">
 Search 
 <kbd>{{formatForDisplay combo}}</kbd> {{! Renders K on Mac, Ctrl+K on Windows/Linux }}
 </button>
 </div>
</template>

Why this is the move:

  • Native TypeScript: It’s written in TS. You get full type safety in your strict GTS templates without hunting for separate @types packages.
  • Pragmatic Defaults: It automatically ignores hotkeys from input-like elements for single keys and Shift/Alt combos, while Ctrl/Meta shortcuts still fire (since those are typically app-level commands).
  • The "Mod" Key: You don't have to check navigator.platform. Mod automatically maps to Command on Mac and Control on Windows.
  • Lifecycle Managed: Your hotkey logic is co-located with your UI. If the component is on screen, the shortcut works. If it’s gone, the listener is gone.

Why not ember-keyboard?

ember-keyboard is battle-tested, but it doesn't ship TypeScript types out of the box—a dealbreaker in a strict .gts setup.

An official @tanstack/hotkeys Ember glue addon (helper, modifier, test helpers) would be a welcome addition. Until then, the ~30 lines above get the job done.


Simple, type-safe, and zero BS. Just the way a Polaris app should be.