この記事は何
「ユーザーが実際に押したキーの組み合わせを取得して、ショートカットとして登録する」UI の実装メモです。keydown で拾うだけ……と思いきや、preventDefault 忘れや修飾キーの扱いで地味にハマります。Mac の ⌘ と Windows の Ctrl を両対応させる方法も含めて、手元で試せる最小コードでまとめます。
ショートカット登録機能や、キーボード操作を受け付けるエディタ系 UI を作るときに必要になる知識です。コピペで動く粒度で書くので、そのまま土台に使えます。
最小の取得コード
取得モード(capturing)のあいだだけ keydown を拾い、押された組み合わせを pending に溜めます。
let capturing = false;
const pending = { mod: false, alt: false, shift: false, key: "" };
document.addEventListener("keydown", (e) => {
if (!capturing) return;
e.preventDefault(); // ① ブラウザ標準のショートカットを止める
// ② 修飾キー単体は「まだ確定じゃない」ので待つ
if (["Meta", "Control", "Alt", "Shift"].includes(e.key)) return;
pending.mod = e.metaKey || e.ctrlKey; // ③ ⌘ と Ctrl を1つにまとめる
pending.alt = e.altKey;
pending.shift = e.shiftKey;
pending.key = normalizeKey(e.key); // ④ e.key を表示用に正規化
capturing = false; // 1つ取れたら終了
});
この4行コメント(①〜④)が、そのままハマりどころです。
ハマりどころ4つ
① preventDefault() しないとブラウザに横取りされる
⌘ + S(保存)や ⌘ + W(タブを閉じる)は、ブラウザ/OS が先に反応します。取得モード中に e.preventDefault() を呼ばないと、登録する前に保存ダイアログが出たりタブが閉じたりします。取得モードのときだけ標準動作を止めるのがポイントです。
② 修飾キー単体の keydown を弾く
⌘ + C を押すとき、ユーザーはまず ⌘ を押し下げます。この瞬間にも keydown が飛び、e.key は "Meta" になります(Ctrl なら "Control")。修飾キーだけの段階では本体キーがまだ来ていないので、確定せずに return して次を待ちます。これを忘れると「⌘ だけのショートカット」が登録されてしまいます。
③ e.metaKey || e.ctrlKey で ⌘ と Ctrl を1つにまとめる
両対応の肝です。Mac で ⌘ を押すと e.metaKey、Windows で Ctrl を押すと e.ctrlKey が true になります。これを OR で1つの論理修飾キー(mod)に畳むと、「どっちの環境で押されたか」を気にせず同じデータに落とせます。別々に持つと、後段で OS 分岐が増えていきます。
④ e.key は小文字・記号で来るので正規化する
e.key には押したキーの「文字」が入りますが、そのままでは表示に向かない値が来ます。
const normalizeKey = (k) => {
if (k === " ") return "Space";
const map = {
ArrowLeft: "←", ArrowRight: "→", ArrowUp: "↑", ArrowDown: "↓",
Backspace: "Delete",
};
if (map[k]) return map[k];
return k.length === 1 ? k.toUpperCase() : k; // "a" → "A"、"F5" はそのまま
};
特にShift を押していない a キーは "a"(小文字)で来るので、表示を揃えるなら toUpperCase() が要ります。スペースは見えない " "、矢印キーは "ArrowLeft" のような長い名前で来るので記号に変換します。
表示する「文字」が欲しいなら e.key、WASD 操作のようにキーの物理位置が欲しいなら e.code(例 "KeyA")を使い分けます。今回は表示目的なので e.key + 正規化です。
取得結果を両 OS で表示する
pending をトークン配列に組み立て、表示のときだけ OS 辞書で記号に変換します。データは OS 非依存のまま持つのがコツです。
const tokensOf = (p) => {
const t = [];
if (p.mod) t.push("mod");
if (p.alt) t.push("alt");
if (p.shift) t.push("shift");
if (p.key) t.push(p.key);
return t; // 例: ["mod", "shift", "Z"]
};
const MAC = { mod: "⌘", alt: "⌥", shift: "⇧" };
const WIN = { mod: "Ctrl", alt: "Alt", shift: "Shift" };
const render = (tokens, os) => {
const map = os === "mac" ? MAC : WIN;
return tokens.map((k) => map[k] ?? k).join(" + ");
};
render(["mod", "shift", "Z"], "mac"); // "⌘ + ⇧ + Z"
render(["mod", "shift", "Z"], "win"); // "Ctrl + Shift + Z"
取得(keydown)と表示(辞書変換)で同じトークン形式を共有しているので、間に変換層を挟まずに済みます。
まとめ
- 取得モードのあいだだけ
keydownを拾い、e.preventDefault()でブラウザの横取りを止める -
e.keyが"Meta"/"Control"などの修飾キー単体は確定せず待つ -
e.metaKey || e.ctrlKeyで ⌘ と Ctrl を論理modに統合すると両対応がラクになる -
e.keyは小文字・記号で来るのでnormalizeKeyで整える(位置が欲しいときはe.code) - データは OS 非依存トークンで持ち、表示の瞬間だけ OS 辞書で記号化する
このアプローチで作ったツール(Photoshop・Illustrator・Figma・Office のショートカットをアプリ別に一覧し、★で自分専用のチートシートを作れる): ショートカットキー チートシート / 使い方ガイド
Web制作・SEOツール開発の技術情報サイト: CodeQuest.work