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
@typespackages. - 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.Modautomatically maps toCommandon Mac andControlon 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)