DEV Community

Cover image for Declarative Hotkeys in Ember with @tanstack/hotkeys
Ignace Maes
Ignace Maes

Posted on

Declarative Hotkeys in Ember with @tanstack/hotkeys

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
Enter fullscreen mode Exit fullscreen mode

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?.();
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)