マガジン

ゲームのメニューをキーボードだけで操作できるようにする

遊びをつくる

執筆にClaudeを使用しています。

キーボード、ゲームパッド、タッチ画面を同じ机に置いた操作方法の比較。
記事のテーマをもとに制作したAI生成イメージ。実際のゲーム画面ではありません。

ゲームを公開したあと、「マウスを使わずに操作したい」という声が届いたことはないでしょうか。キーボードだけでメニューを操作できるかどうかは、プレイしやすさに直結します。特にブラウザゲームでは、PCユーザーがキーボードのTabキーやEnterキーで素早く動かすことを好む場合が少なくありません。 キーボード操作への対応は、アクセシビリティの観点でも重要です。マウス操作が難しいユーザー、ゲームパッドをキーボードにマッピングしているユーザーにとって、キーボードだけで完結するメニューは「遊べるか遊べないか」の境界線になることがあります。 この記事では、HTMLとJavaScriptで作ったブラウザゲームのメニューを対象に、キーボード操作を実装するための考え方・手順・よくある失敗をまとめます。フレームワークは問わず、素のDOM操作を前提として説明しますので、どんな構成のゲームにも応用できます。

この記事の目次
  1. まずフォーカス管理を理解する
  2. 矢印キーでの選択移動を実装する
  3. フォーカストラップ:メニューが開いている間だけ閉じ込める
  4. フォーカスの見た目を消さない
  5. Escapeキーでメニューを閉じる
  6. 次の行動
  7. 参考資料

まずフォーカス管理を理解する

キーボード操作の核心は「フォーカス」です。フォーカスとは、現在キーボード入力を受け付けている要素のことです。ブラウザはTabキーを押すたびにフォーカスを次の要素へ移し、EnterキーまたはSpaceキーでその要素をアクティブにします。

<button>や<a>はデフォルトでフォーカスを受け取れますが、<div>や<span>で作ったカスタムボタンは受け取れません。メニュー項目を<div>で実装している場合、tabindex="0"属性を追加するだけでフォーカス対象になります。

<div class="menu-item" tabindex="0" role="button">はじめから</div>

role="button"はスクリーンリーダー向けの意味付けです。視覚だけでなくアシスト技術にも「これはボタンだ」と伝えられます。ただしroleを付けただけでは動作は変わらないため、後述するキーイベントの処理は別途必要です。

矢印キーでの選択移動を実装する

TabキーとEnterキーだけでも基本的な操作は成り立ちますが、ゲームのメニューらしい操作感を出すには、矢印キーで項目を移動できるようにするのが一般的です。この仕組みを「ローバーtabindex」と呼びます。

考え方はシンプルです。

  1. メニュー項目をリストとして管理し、現在選択中のインデックスを変数で持ちます。
  2. keydownイベントでArrowDown・ArrowUpを検知し、インデックスを増減させます。
  3. インデックスが変わるたびに、全項目のtabindexを-1にリセットしてから、選択中の項目だけtabindex="0"に戻し、focus()を呼びます。
javascript
let currentIndex = 0;
const items = document.querySelectorAll('.menu-item');

function moveFocus(newIndex) {
  items[currentIndex].tabIndex = -1;
  currentIndex = (newIndex + items.length) % items.length;
  items[currentIndex].tabIndex = 0;
  items[currentIndex].focus();
}

document.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown') moveFocus(currentIndex + 1);
  if (e.key === 'ArrowUp')   moveFocus(currentIndex - 1);
  if (e.key === 'Enter' || e.key === ' ') items[currentIndex].click();
});

(newIndex + items.length) % items.lengthという計算は、リストの末尾から先頭へ・先頭から末尾へ折り返すための書き方です。メニューが循環するため、端で止まるより自然な操作感になります。

Enterキーで.click()を呼ぶのは、既存のクリックハンドラをそのまま再利用するためです。マウス用とキーボード用で別々にロジックを書く必要がなくなります。

フォーカストラップ:メニューが開いている間だけ閉じ込める

ゲームでは「設定画面を開いた→Tabを連打したらゲーム本体のボタンにフォーカスが移ってしまった」という問題がよく起きます。これを防ぐのがフォーカストラップです。

フォーカストラップは、フォーカスを特定のコンテナ内に閉じ込める処理です。実装の要点は二つあります。

  • コンテナ内のフォーカス可能な要素を全て取得し、最初と最後の要素を把握しておきます。
  • 最後の要素でTabを押したら最初の要素へ、最初の要素でShift+Tabを押したら最後の要素へ強制移動します。
javascript
function trapFocus(container) {
  const focusable = container.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last  = focusable[focusable.length - 1];

  container.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) { e.preventDefault(); last.focus(); }
    } else {
      if (document.activeElement === last)  { e.preventDefault(); first.focus(); }
    }
  });
}

メニューを開くときにtrapFocus(menuElement)を呼び、メニューを閉じるときはイベントリスナーをremoveEventListenerで取り外すか、AbortControllerで管理すると後始末が楽になります。

フォーカストラップはモーダルダイアログや一時的なオーバーレイメニューで特に重要です。ゲームのタイトル画面のような「常に表示されているメニュー」では必須ではありませんが、ポーズ画面や設定パネルには必ず実装することを検討してください。

フォーカスの見た目を消さない

実装上、見落とされやすい失敗がフォーカスリングの非表示化です。デザイン上「青い枠が邪魔」という理由でoutline: noneをCSS全体に当てているプロジェクトをよく見かけます。これをやるとキーボードユーザーは現在どの項目を選んでいるか分からなくなります。

対策は二つあります。一つは、outline: noneを使うかわりに独自のフォーカススタイルを定義することです。

css
.menu-item:focus {
  outline: 2px solid #ffcc00;
  outline-offset: 2px;
}

もう一つは、:focus-visible疑似クラスを使うことです。マウスクリックのときはフォーカスリングを表示せず、キーボード操作のときだけ表示するという制御がCSSだけで書けます。

css
.menu-item:focus { outline: none; }
.menu-item:focus-visible { outline: 2px solid #ffcc00; }

ゲームの配色に合わせた目立つ色を使うと、フォーカスリングがUIの一部として自然に溶け込みます。白背景のゲームなら濃い色、暗い背景なら明るい色を選ぶと視認性が上がります。

Escapeキーでメニューを閉じる

キーボードユーザーが特に期待している操作の一つが、Escapeキーによるキャンセルです。設定画面を開いたらEscapeで戻れる、確認ダイアログが出たらEscapeでキャンセルできる、という動作は多くのソフトウェアで標準的なため、対応していないと操作の流れが途切れます。

実装は単純です。メニューが開いているフラグを持ち、keydownでEscapeを検知したときに閉じる処理を呼ぶだけです。

javascript
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && isMenuOpen) closeMenu();
});

メニューを閉じたあとは、メニューを開く前にフォーカスがあった要素に戻します。openMenu()を呼ぶ直前にconst trigger = document.activeElementで保存しておき、closeMenu()の末尾でtrigger.focus()を呼ぶのがシンプルな方法です。これを忘れると、閉じたあとにフォーカスが宙に浮き、ユーザーがどこにいるか分からなくなります。

次の行動

まず自分のゲームのメニュー項目を開発者ツールで確認し、Tabキーだけで全項目を巡回できるかテストしてみてください。フォーカスが当たらない要素があれば、tabindex="0"の追加から始めるのが最小の一歩です。矢印キー移動とEscapeキーの対応を追加したら、キーボードだけでタイトルからゲーム開始まで完結するかどうかを通しで確認してみましょう。完成したゲームはアソバースにZIPまたはHTMLファイルとして投稿し、実際のブラウザ環境での動作を確かめてください。

参考資料

  • WAI-ARIA Authoring Practices Guide(W3C)
  • MDN Web Docs「キーボードナビゲーション」
  • MDN Web Docs「:focus-visible」