keydownイベントとは、キーが押し下げられた瞬間に発生するイベントで、押されたキーは event.key(入力される文字やキー名)と event.code(キーの物理的な位置)で判定します。 Enterで送信、Escapeで閉じる、Ctrl+Sで保存といったキーボード操作は、すべてこの keydown を入り口にして作ります。

日本語サイトで特に大切なのが、変換を確定するEnterで送信まで走ってしまう現象への対策です。この記事では、その現象を Chromium 141 上で再現し、isComposing と keyCode === 229 を併用した判定で Chrome・Firefox・Safari のどのイベント順でも送信されないことを確かめました。あわせて、event.key と event.code の値一覧、ショートカット判定の完成形コードを、実行結果つきで載せています。

検証環境は Node.js 22 + Playwright 1.56.0 + Chromium 141.0.7390.37(ヘッドレス、Linux)です。日本語変換は Chrome DevTools Protocol(CDP)の IME 用コマンドで再現しており、Firefox と Safari の実機での確認は未実行です(両ブラウザの挙動は MDN と W3C UI Events、後述の検証記事で確認した内容として区別して書きます)。

keydownイベントの早見表

やりたいこと 書き方 補足
キーが押されたら処理する el.addEventListener('keydown', (e) => { ... }) ページ全体なら document に付ける
Enterかどうか判定する e.key === 'Enter' テンキーのEnterも key は Enter
日本語変換中のキーを除外する if (e.isComposing || e.keyCode === 229) return; 3ブラウザ共通で使える判定
Escapeで閉じる e.key === 'Escape' Esc ではなく Escape
矢印キーを判定する e.key === 'ArrowUp' など Down / Left / Right も同じ形
スペースキーを判定する e.key === ' ' または e.code === 'Space' key は半角スペース1文字
Ctrl+S(MacはCmd+S) (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's' e.preventDefault() で保存ダイアログを止める
Shift+Enterは改行、Enterは送信 if (e.key === 'Enter' && !e.shiftKey) テキストエリアの定番
押しっぱなしの連続実行を防ぐ if (e.repeat) return; 2回目以降の keydown は repeat が true
入力欄にいる間はショートカットを無視 e.target の tagName と isContentEditable を見る 完成形は後半に掲載
ブラウザ既定の動作を止める e.preventDefault() スクロール、改行、フォーム送信など
キーの位置で判定する(WASDなど) e.code === 'KeyW' キーボード配列が違っても同じ位置

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript keydownイベントの使い方|Enter判定とIME対策」

keydown・keyup・keypress の違いと発火順

キーボードのイベントは3種類あります。新しく書くコードでは keydown と keyup の2つを使い、keypress は使わないのが基本です(keypress は非推奨で、Enter や文字キー以外では発生しません)。

イベント 発生するタイミング 押しっぱなし 主な用途
keydown キーを押し下げた瞬間 繰り返し発生(repeat が true) ショートカット、Enter送信、既定動作の停止
keyup キーを離した瞬間 1回だけ 離したタイミングの検知、ゲームのキー解放
keypress(非推奨) 文字が入力されるキーを押したとき 繰り返し発生 既存コードの読解のみ
input 入力欄の値が変わった後 値が変わるたび 文字数カウント、入力内容の検証

入力欄で a を1回押したときの実際の発火順です。

keydown   key=a code=KeyA keyCode=65 isComposing=false repeat=false
keypress  key=a code=KeyA keyCode=97 isComposing=false repeat=false
beforeinput data=a
input       data=a
keyup     key=a code=KeyA keyCode=65 isComposing=false repeat=false

keydown の時点では、入力欄の値はまだ変わっていません。 値が反映された後に処理したい場合(文字数カウントなど)は input イベントを使います。逆に「この文字を入力させたくない」「Enterで改行させたくない」といった既定動作の停止は、keydown で preventDefault() を呼びます。

keydownを受け取れる要素

keydown はフォーカスを持っている要素で発生し、そこから親へ伝わります(バブリング)。フォーカスできない要素に付けても、その要素では発生しません。

<div id="plain">div</div>
<div id="tab" tabindex="0">tab</div>
<input id="i">
<script>
  for (const id of ['plain', 'tab', 'i']) {
    document.getElementById(id).addEventListener('keydown', (e) => console.log(id + ':' + e.key));
  }
  document.addEventListener('keydown', (e) =>
    console.log('document:' + e.key + ' target=' + (e.target.id || e.target.tagName)));
</script>

それぞれをクリックしてから a、b、c を押した実行結果です。

document:a target=BODY
tab:b
document:b target=tab
i:c
document:c target=i
付ける場所 受け取れる条件 向いている用途
document ページ内のどこにフォーカスがあっても届く ページ全体のショートカット
input / textarea その入力欄にフォーカスがあるとき Enter送信、入力の制御
tabindex="0" を付けた要素 その要素にフォーカスがあるとき 自作のリスト、タブ、メニュー
tabindex のない div 届かない(上の結果の1行目) 親か document に付ける

event.key と event.code の値一覧

event.key は「入力される文字・キーの意味」、event.code は「キーの物理的な位置」を表します。次の表は Chromium 141 で実際にキーを押して記録した値です(keyCode は既存コードを読むときの参考用)。

押したキー event.key event.code keyCode(非推奨)
Enter Enter Enter 13
テンキーのEnter Enter NumpadEnter 13
Escape Escape Escape 27
Tab Tab Tab 9
スペース 半角スペース1文字 Space 32
Backspace Backspace Backspace 8
Delete Delete Delete 46
↑ ArrowUp ArrowUp 38
↓ ArrowDown ArrowDown 40
← ArrowLeft ArrowLeft 37
→ ArrowRight ArrowRight 39
Home / End Home / End Home / End 36 / 35
PageUp / PageDown PageUp / PageDown PageUp / PageDown 33 / 34
A a KeyA 65
Shift+A A KeyA 65
1 1 Digit1 49
-(ハイフン) - Minus 189
/(スラッシュ) / Slash 191
F1 / F5 F1 / F5 F1 / F5 112 / 116
Shift Shift ShiftLeft 16
Ctrl Control ControlLeft 17
Alt(MacのOption) Alt AltLeft 18
Windowsキー / MacのCmd Meta MetaLeft 91
CapsLock CapsLock CapsLock 20
Insert Insert Insert 45
日本語変換中のキー(参考) Process 押したキーの位置 229

表から読み取れるポイントは3つです。

  • スペースの key は 'Space' ではなく半角スペース1文字です。e.key === ' ' と書くか、e.code === 'Space' を使います。
  • key は Shift や配列の影響を受けます。同じキーでも a と A に変わるため、ショートカット判定では e.key.toLowerCase() で小文字にそろえます。
  • 修飾キー単体でも keydown は発生します。Ctrl+S を押すと、Control の keydown、続いて s の keydown(ctrlKey が true)の順に届きます。

最後の「日本語変換中のキー」の行だけは実際のキー入力ではなく、Chrome が変換中に返す値として知られる Process と 229 を CDP で送って再現したものです。

テンキーの数字は NumLock の状態で key が変わります。今回の環境(NumLock オフ相当)では、テンキーの1は key が End、code が Numpad1 でした。

key と code の使い分け

判定したいもの 使うプロパティ 理由
Enter、Escape、矢印、Tab などの機能キー key 名前がそのまま読める
Ctrl+S などの文字ショートカット key(小文字にそろえる) キーボード配列が違っても「S の文字」で判定できる
WASD 移動などキーの位置が大事な操作 code 配列が違っても同じ位置のキーになる
左右の Shift や Ctrl の区別 code ShiftLeft と ShiftRight で分かれる
テンキーの Enter だけ区別 code NumpadEnter で判定できる
既存コードの数値判定 keyCode 非推奨。新規では IME 判定の 229 だけに使う

修飾キーが押されているかは、次の4つの真偽値で調べます。

プロパティ 対応するキー
e.ctrlKey Ctrl(Mac では control)
e.metaKey Mac の Cmd、Windows の Windows キー
e.shiftKey Shift
e.altKey Alt(Mac では Option)

Ctrl+Shift+S を押したときの s の keydown は、実行結果では次の値になりました。

key="s" code=KeyS ctrl=true shift=true alt=false meta=false

Enterキーを判定する基本コード

もっとも基本の形です。入力欄で Enter が押されたら処理します。

<input id="keyword" type="text" placeholder="キーワードを入力して Enter">
<script>
  const keyword = document.querySelector('#keyword');
  keyword.addEventListener('keydown', (e) => {
    if (e.isComposing || e.keyCode === 229) return; // 日本語変換中は何もしない
    if (e.key !== 'Enter') return;
    e.preventDefault();
    console.log('検索: ' + keyword.value);
  });
</script>

2行目の isComposing と keyCode === 229 の判定が、次の章で説明する日本語変換への対策です。Enter 判定を書くときは、この1行を必ずセットにすると覚えておくと安心です。

フォーム内のEnterは、何も書かなくても送信される

form の中の1行入力欄で Enter を押すと、ブラウザが自動でフォームを送信します。これは keydown のコードとは別の、ブラウザ標準の動作です。入力欄の Enter では送信させたくない場合は、keydown で既定動作を止めます。

<form id="g">
  <input id="n2"><input id="m2"><button>登録</button>
</form>
<script>
  const g = document.getElementById('g');
  g.addEventListener('submit', (e) => { e.preventDefault(); console.log('g submit'); });
  g.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && e.target instanceof HTMLInputElement) e.preventDefault();
  });
</script>

対策なしのフォームと並べて確かめた結果です。

操作 対策なしのフォーム 上のコードを入れたフォーム
入力欄で Enter submit が発生 発生しない
ボタンにフォーカスして Enter submit が発生 submit が発生(意図どおり)

日本語変換の確定Enterで送信される現象を再現する

日本語を入力するとき、読みを打ってから Enter で変換を確定します。この確定の Enter も keydown として届くため、Enter で送信するコードがそのまま反応し、文章の途中で送信されてしまうことがあります。チャットや検索欄でよく見かける現象です。

確かめるために、判定の書き方を3種類用意しました。

// A: Enter だけを見る
(e) => { if (e.key === 'Enter' || e.keyCode === 13) send(); }

// B: isComposing だけで変換中を除外する
(e) => { if (e.isComposing) return; if (e.key === 'Enter') send(); }

// C: isComposing と keyCode 229 を併用する
(e) => { if (e.isComposing || e.keyCode === 229) return; if (e.key === 'Enter') send(); }

それぞれに、変換確定の Enter を2通りのイベント順で送りました。1つ目は「keydown が先、compositionend が後」(Chrome・Firefox の順)、2つ目は「compositionend が先、keydown が後」(Safari の順)です。Chromium 141 上で CDP の Input.imeSetComposition、Input.insertText、Input.dispatchKeyEvent を使って再現しています。

### A: key===Enter のみ
  chrome: 送信0回  [keydown key=Process keyCode=229 isComposing=true / compositionend]
  safari: 送信1回  [compositionend / keydown key=Enter keyCode=229 isComposing=false]
  plain:  送信1回  [keydown key=Enter keyCode=13 isComposing=false]
### B: isComposing のみ
  chrome: 送信0回  [keydown key=Process keyCode=229 isComposing=true / compositionend]
  safari: 送信1回  [compositionend / keydown key=Enter keyCode=229 isComposing=false]
  plain:  送信1回  [keydown key=Enter keyCode=13 isComposing=false]
### C: isComposing + keyCode 229
  chrome: 送信0回  [keydown key=Process keyCode=229 isComposing=true / compositionend]
  safari: 送信0回  [compositionend / keydown key=Enter keyCode=229 isComposing=false]
  plain:  送信1回  [keydown key=Enter keyCode=13 isComposing=false]

(chrome・safari は再現したイベント順の名前、plain は変換なしの通常の Enter です)

判定の書き方 Chrome・Firefox の順 Safari の順 通常の Enter
A: Enter だけを見る 送信されない 確定で送信されてしまう 送信される
B: isComposing だけ 送信されない 確定で送信されてしまう 送信される
C: isComposing と 229 を併用 送信されない 送信されない 送信される

3つのイベント順すべてで意図どおりに動いたのは C だけでした。B の isComposing だけでは、compositionend が先に届く順のときに isComposing が false になり、確定の Enter が通常の Enter と同じに見えてしまいます。

なお A について、Chrome・Firefox の順では key を Process(Chrome が変換中に返す値として知られるもの)として送ったため送信されませんでした。変換中の key はブラウザや OS によって Enter になる場合もあります。A の形に頼らず、C の形で明示的に除外するのが確実です。

ブラウザ別のイベント発火順

変換確定の Enter を押したときの順序を整理します。

ブラウザ イベントの順序 確定Enterの keydown の isComposing keyCode 確認方法
Chrome / Edge keydown → compositionend → keyup true 229 Chromium 141 で CDP により再現
Firefox(65以降) keydown → compositionend → keyup true 229 MDN の記載で確認(実機は未実行)
Safari compositionend → keydown → keyup false 229 MDN の注記と検証記事で確認(実機は未実行)

W3C の UI Events 仕様では、変換中の順序は「keydown、compositionstart、compositionupdate、compositionend、keyup」とされ、isComposing は「compositionstart の後、対応する compositionend の前」に発生したキーイベントで true になると定義されています。Safari の順では、確定の keydown が compositionend の後に届くため、定義どおり false になります。

MDN の keydown のページにも、次の趣旨の注記があります。

  • 変換を閉じる最後のキーでは、compositionend が keydown より先に発生することがあり、その場合 isComposing は false になる
  • その場合でも keyCode は 229 のままなので、非推奨ではあるものの keyCode も併せて確認するのがよい
  • Firefox は 65 から、変換中にも keydown と keyup を発生させるようになった

変換を始める最初の1文字でも同じことが起きます。今回の再現では、最初のキーの keydown は compositionstart より前に届き、isComposing は false、keyCode は 229 でした。

keydown key=Process code=KeyA keyCode=229 isComposing=false
compositionstart
compositionupdate data=あ
input data=あ isComposing=true
keyup key=a code=KeyA keyCode=65 isComposing=true

変換の最初と最後は isComposing だけでは拾えない場合があり、229 の併用がその両方を補います。

keyCode 229 とは

keyCode は非推奨のプロパティですが、229 は「このキー操作は IME(日本語入力などの変換ソフト)が処理した」ことを表す特別な値です。通常の Enter は 13、変換確定の Enter は 229 になるため、数値だけで区別できます。

状況 key keyCode isComposing
通常の Enter Enter 13 false
変換中の文字キー・確定 Enter(Chrome の順) Process 229 true
確定 Enter(Safari の順) Enter 229 false

「非推奨のプロパティを使ってよいのか」と迷うところですが、MDN 自身が併用を勧めており、現時点では 229 の判定に代わる標準の手段がありません。IME 判定の1か所だけに閉じ込めて使うのがおすすめです。

// 変換中のキー操作かどうか(この関数にだけ keyCode を閉じ込める)
const isImeEvent = (e) => e.isComposing || e.keyCode === 229;

compositionstart と compositionend でフラグを持つ方法

変換の開始と終了を自分で記録する方法もあります。

let composing = false;
input.addEventListener('compositionstart', () => { composing = true; });
input.addEventListener('compositionend', () => { composing = false; });
input.addEventListener('keydown', (e) => {
  if (composing) return;
  if (e.key === 'Enter') send();
});

この方法は Chrome・Firefox の順では働きますが、compositionend が先に届く順では、keydown の時点でフラグがすでに false に戻っています。仕組みとしては B(isComposing のみ)と同じ結果になるため、フラグ方式を使う場合も e.keyCode === 229 の確認を足しておきます。変換中の表示切り替え(確定前の文字に下線を出すなど)には、composition 系のイベントが向いています。

コピペ用の完成形:Enter送信とショートカット

ここまでの内容をまとめた、そのまま保存して開ける HTML です。Enter で送信、Shift+Enter で改行、変換確定の Enter は無視、Ctrl+S(Mac は Cmd+S)などのショートカットに対応しています。

<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<title>keydown デモ</title>
<form id="chat">
  <textarea id="msg" rows="2" placeholder="Enterで送信、Shift+Enterで改行"></textarea>
  <button type="submit">送信</button>
</form>
<input id="search" type="search" placeholder="検索(/ でフォーカス)">
<ul id="log"></ul>

<script>
const log = (text) => {
  const li = document.createElement('li');
  li.textContent = text;
  document.querySelector('#log').append(li);
};

// 1) 変換中のキー操作かどうか(Chrome・Firefox・Safari 共通)
const isImeEvent = (e) => e.isComposing || e.keyCode === 229;

// 2) Enterで送信、Shift+Enterで改行
const form = document.querySelector('#chat');
const msg = document.querySelector('#msg');

msg.addEventListener('keydown', (e) => {
  if (isImeEvent(e)) return;        // 変換確定のEnterは通さない
  if (e.key !== 'Enter') return;
  if (e.shiftKey) return;           // Shift+Enter は改行のまま
  e.preventDefault();               // 改行が入らないようにする
  if (e.repeat) return;             // 押しっぱなしの連続送信を防ぐ
  form.requestSubmit();
});

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const text = msg.value.trim();
  if (text === '') return;
  log('送信: ' + text);
  msg.value = '';
});

// 3) ショートカット
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);

const isEditable = (el) =>
  el instanceof HTMLElement &&
  (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName));

const shortcuts = [
  { key: 's', mod: true, allowInInput: true, run: () => log('保存') },
  { key: 'z', mod: true, shift: true, run: () => log('やり直し') },
  { key: 'k', mod: true, allowInInput: true, run: () => document.querySelector('#search').focus() },
  { key: '/', run: () => document.querySelector('#search').focus() },
  { key: '?', run: () => log('ヘルプ') },
  { key: 'Escape', allowInInput: true, run: () => document.activeElement.blur() },
];

document.addEventListener('keydown', (e) => {
  if (isImeEvent(e)) return;        // 変換中は何もしない
  if (e.altKey) return;

  const mod = isMac ? e.metaKey : e.ctrlKey;          // Cmd か Ctrl
  const other = isMac ? e.ctrlKey : e.metaKey;
  if (other) return;
  const key = e.key.length === 1 ? e.key.toLowerCase() : e.key;

  const hit = shortcuts.find((s) =>
    s.key === key &&
    Boolean(s.mod) === mod &&
    (!s.mod || Boolean(s.shift) === e.shiftKey));     // 修飾キーつきは Shift も厳密に比べる
  if (!hit) return;
  if (isEditable(e.target) && !hit.allowInInput) return; // 入力中は文字入力を優先

  e.preventDefault();               // ブラウザ既定の動作(保存ダイアログなど)を止める
  if (e.repeat) return;             // 押しっぱなしでも実行は1回だけ
  hit.run();
});
</script>
</html>

実行結果

このファイルを Chromium 141 で開き、Playwright からキーを押して確かめた結果です(変換確定の2行は CDP による再現)。

操作 ログに出た内容 フォーカス defaultPrevented
hello、Shift+Enter、world、Enter 送信: hello(改行)world msg true
空のまま Enter なし msg true
abc を入力して Enter を押しっぱなし(keydown 3回) 送信: abc(1回だけ) msg true
変換確定の Enter(Chrome・Firefox の順) なし msg false
変換確定の Enter(Safari の順) なし msg false
確定後にもう一度 Enter 送信: こんにちは世界 msg true
Ctrl+S 保存 body true
Ctrl+Shift+S なし body false
Ctrl+Shift+Z やり直し body true
Ctrl+Z なし body false
Ctrl+Alt+S なし body false
S だけ なし body false
Ctrl+S を押しっぱなし(keydown 4回) 保存(1回だけ) body true
?(Shift+/) ヘルプ body true
/ なし(検索欄へ移動) search true
検索欄で / なし(文字として入力される) search false
検索欄で Ctrl+S 保存 search true
検索欄で Escape なし(フォーカスが外れる) body true
テキストエリアで Ctrl+K なし(検索欄へ移動) search true

navigator.platform を MacIntel に差し替えた状態でも確かめました。

操作(Mac 相当) ログに出た内容 defaultPrevented
Cmd+S 保存 true
Ctrl+S なし false
Cmd+Shift+Z やり直し true

変換確定の Enter では defaultPrevented が false のままです。変換中のキーには手を出さず、IME にそのまま処理してもらうのがポイントで、ここで preventDefault() を呼ぶと確定そのものに影響することがあります。

ショートカット判定の4つのポイント

完成形コードの中身を、判断の理由とあわせて説明します。

1. 修飾キーは「押されていること」と「押されていないこと」の両方を見る

e.ctrlKey && e.key === 's' だけだと、Ctrl+Shift+S や Ctrl+Alt+S でも反応します。別の機能を割り当てたくなったときに衝突するため、使わない修飾キーが押されていないことも確認します。

// 直す前:Ctrl+Shift+S でも Ctrl+Alt+S でも反応する
if (e.ctrlKey && e.key === 's') save();

// 直した後:Ctrl+S(Mac は Cmd+S)ちょうどのときだけ反応する
const mod = isMac ? e.metaKey : e.ctrlKey;
if (mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 's') {
  e.preventDefault();
  save();
}

Mac と Windows を分けずに (e.ctrlKey || e.metaKey) と書く方法も手軽です。厳密さより短さを優先したい小さなツールなら、この形で十分に使えます。

? や / のような記号キーは、配列によって Shift が必要だったり不要だったりします。完成形コードでは、修飾キーなしのショートカットは Shift の状態を比べず、key の文字だけで判定しています。

2. preventDefault はブラウザの既定動作と重なるキーで呼ぶ

キー ブラウザの既定動作 preventDefault すると
Ctrl+S / Cmd+S ページの保存ダイアログ ダイアログが出ず、自前の保存だけ動く
テキストエリアの Enter 改行の入力 改行が入らない
フォーム内入力欄の Enter フォームの送信 送信されない
スペース、矢印、PageDown ページのスクロール スクロールしない
Tab 次の要素へフォーカス移動 移動しない(キーボード操作の妨げになりやすいので慎重に)
/ 文字の入力 入力されない

preventDefault() は自分のショートカットに一致したときだけ呼びます。keydown の先頭で無条件に呼ぶと、文字入力も Tab 移動もできなくなります。

なお、Ctrl+W(タブを閉じる)、Ctrl+T(新しいタブ)、Ctrl+N(新しいウィンドウ)など、ブラウザ自身の操作に使われる組み合わせは、ページ側から止められないものとして一般に知られています(今回は未実行)。ショートカットは、こうしたキーと重ならない組み合わせを選びます。

3. repeat は preventDefault の後で確認する

キーを押し続けると、keydown が繰り返し発生し、2回目以降は e.repeat が true になります。

keydown key=a keyCode=65 repeat=false
keydown key=a keyCode=65 repeat=true
keydown key=a keyCode=65 repeat=true
keyup   key=a keyCode=65 repeat=false

保存や送信は1回で十分なので repeat を除外します。ここで順番が大切です。最初に書いたコードでは、関数の先頭に if (e.repeat) return; を置いていました。

// 直す前:repeat のときは preventDefault まで届かない
document.addEventListener('keydown', (e) => {
  if (e.repeat) return;
  // ...一致するショートカットを探す...
  e.preventDefault();
  hit.run();
});

この形で Ctrl+S を押しっぱなしにすると、保存の実行は1回でしたが、2回目以降の keydown の defaultPrevented は false でした。つまり、押し続けている間にブラウザの保存ダイアログが開く余地が残ります。

// 直した後:既定動作は毎回止め、実行だけ1回にする
e.preventDefault();
if (e.repeat) return;
hit.run();

この順にすると、押しっぱなしの4回すべてで defaultPrevented が true、実行は1回になりました(実行結果の表のとおり)。矢印キーでの移動のように、押し続けて連続で動かしたい操作では repeat を除外しません。

4. 入力欄にフォーカスがある間は、文字入力を優先する

/ や ? のような1文字のショートカットは、入力欄で文字を打っているときにも反応してしまうと困ります。keydown の e.target が入力できる要素かどうかを見て切り分けます。

const isEditable = (el) =>
  el instanceof HTMLElement &&
  (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName));
ショートカットの種類 入力欄にいるとき 例
1文字キー(修飾キーなし) 無視する /、?、j、k
Ctrl / Cmd つき 多くは有効のままでよい Ctrl+S、Ctrl+K
Ctrl / Cmd つきで編集と重なるもの 無視する Ctrl+Z、Ctrl+A、Ctrl+C
Escape 有効のまま 入力欄からフォーカスを外す、モーダルを閉じる

完成形コードでは、ショートカットごとに allowInInput を持たせています。実行結果でも、検索欄で / を押したときは文字として入力され、Ctrl+S は入力中でも保存が動きました。

よくあるつまずきと直し方

症状 原因 直し方
変換確定の Enter で送信される 変換中の keydown を除外していない if (e.isComposing || e.keyCode === 229) return; を先頭に置く
Chrome では動くが Safari では確定で送信される isComposing だけで判定している keyCode === 229 を併用する
スペースキーが判定できない e.key === 'Space' と書いている e.key === ' ' か e.code === 'Space'
Escape が判定できない 'Esc' と書いている 'Escape'
Shift を押すとショートカットが効かない key が大文字の S になっている e.key.toLowerCase() でそろえる
div に付けた keydown が反応しない フォーカスできない要素に付けている tabindex="0" を付けるか document に付ける
保存ダイアログが出てしまう preventDefault() を呼んでいない、または repeat で先に抜けている 一致したら毎回 preventDefault() を呼ぶ
押しっぱなしで何度も送信される repeat を見ていない if (e.repeat) return;
入力中に / が入力できない 入力欄でもショートカットが反応している e.target が入力要素なら抜ける
Mac で Ctrl+S になってしまう ctrlKey だけを見ている Mac では metaKey を見る
keydown で取った入力欄の値が1文字足りない keydown は値が変わる前に発生する 値を使う処理は input イベントで行う

書く前のチェックリスト

  • Enter を判定する場所に、IME の除外(isComposing と 229)が入っているか
  • 判定は key(意味)と code(位置)のどちらが合っているか
  • 使わない修飾キーが押されていないことまで確認しているか
  • preventDefault() は一致したときだけ呼んでいるか
  • repeat の扱い(1回だけか、連続で動かすか)を決めたか
  • 入力欄にフォーカスがあるときの扱いを決めたか
  • Mac の Cmd と Windows の Ctrl の両方で試したか
  • スマートフォンのソフトウェアキーボードでは keydown の key が Unidentified や 229 になる場合があるため、文字の検知は input イベントで行っているか(一般に知られた挙動。今回は未実行)

よくある質問

Q. keyCode は非推奨なのに、229 の判定に使ってよいのですか? A. IME 判定に限って使うのがおすすめです。MDN の keydown のページでも、event.isComposing || event.keyCode === 229 の形が紹介されています。compositionend が keydown より先に届くイベント順では isComposing が false になるため、今回の再現でも 229 を併用した書き方だけがすべての順序で送信を防げました。isImeEvent のような小さな関数にまとめておくと、将来の書き換えも1か所で済みます。

Q. keydown と keyup のどちらで Enter を判定すればよいですか? A. keydown がおすすめです。改行やフォーム送信といった既定動作を止められるのは keydown の時点です。keyup はキーを離した後に届くため、その時点では改行や送信がすでに済んでいます。

Q. event.key と event.code はどちらを使えばよいですか? A. 「Enter」「Escape」「S の文字」のように意味で判定したいときは key、WASD のようにキーの位置で判定したいときは code を使います。ショートカットは多くの場合 key を小文字にそろえて判定すると、キーボード配列が違う環境でも同じ文字のキーで動きます。

Q. React や Vue でも同じ対策が必要ですか? A. 必要です。フレームワークのイベントでも元になっているのはブラウザの keydown なので、同じ判定を入れます。React では合成イベントの e.nativeEvent.isComposing と e.keyCode を参照する書き方が知られています(フレームワーク上での動作は今回未実行)。

まとめ

  • キー操作は keydown で受け、e.key(意味)と e.code(位置)で判定する。keypress は非推奨
  • Enter 判定には if (e.isComposing || e.keyCode === 229) return; を必ず添える
  • isComposing だけでは、compositionend が先に届く順(Safari の順)で確定 Enter が通ってしまう。229 の併用で3つの順序すべてを防げた
  • スペースの key は半角スペース、Escape は 'Escape'。文字キーは toLowerCase() でそろえる
  • ショートカットは、修飾キーの有無を厳密に比べ、一致したときだけ preventDefault() を呼ぶ
  • repeat の確認は preventDefault() の後に置くと、押しっぱなしでも既定動作を止めたまま実行を1回にできる
  • 入力欄にフォーカスがある間は、1文字ショートカットを無視して文字入力を優先する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「JavaScript keydownイベントの使い方|Enter判定とIME対策」

株式会社RJCでは、keydownイベントやIME対策のようなブラウザの細かな挙動を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?