# SpaceキーやPage Downでは先へ進めない全画面セクション

Source: https://easeweb.dev/learn/scroll-hijacking
Topics: キーボードとフォーカス > Spaceキー、Page Down、矢印キーでページをスクロールできる (WCAG 2.1.1)
The fix: スクロールの固定とホイールのハンドラーを外してブラウザーにスクロールさせ、セクションで止めたいときはscroll-snap-typeを使います。スクリプトを残すなら、ホイールと同じ関数をキーからも呼び、フォーカスを受けたセクションを表示します。最小の変更は、セクションごとに本物のボタンを置くことですが、スクロールのキーは動かないままです。
Test with: keyboard, zoom, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F54 https://www.w3.org/TR/css-scroll-snap-1/ https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type https://developer.mozilla.org/en-US/docs/Web/CSS/overflow https://developer.mozilla.org/en-US/docs/Web/CSS/visibility https://gsap.com/docs/v3/Plugins/Observer/ https://swiperjs.com/swiper-api https://alvarotrigo.com/fullPage/docs/ https://www.nngroup.com/articles/illusion-of-completeness/

よくある問題を再現した学習用のサンプルです。実在のクライアントの診断結果ではありません。コードは出発点となる実装例のため、実際の製品でも確認してください。ここでは支援技術での完全なテストを実施済みとは主張していません。

## 場面

ヘッドホンの製品ページが、全画面の4つのセクションでできています。製品名、バッテリー、修理、そして価格と「今すぐ注文」のリンクです。マウスホイールを1目盛り回すか、1回スワイプすると、次のセクションまで一気に切り替わり、途中で止まることはありません。発売時のページによくある見せ方で、ホイールで操作すると気持ちよく動きます。

キーボードを使う人がページを開いてSpaceキーを押しても、何も動きません。Page Down、下矢印キー、Endも同じです。Tabキーを押すと、最後のセクションの「今すぐ注文」へ直接移ります。ページの中で最初にフォーカスを受けられるのがこのリンクだからです。バッテリーと修理のセクションは、一度も画面に出ないまま通り過ぎます。

最初の画面には、続きがあることを示すものがありません。`overflow: hidden` でスクロールバーはなくなり、ウィンドウいっぱいのセクションには、下の端で切れて見えるものもありません。

## 確認できる問題

達成基準2.1.1は、すべての機能をキーボードで操作できることを求めています。ふつうは何もしなくても、スクロールできるページならSpace、Page Down、矢印キー、Home、Endでスクロールできます。このページでは、そのスクロールが止められています。`html` と `body` に `overflow: hidden` を指定しているのでブラウザーにはスクロールできるものがなく、セクションを動かす `transform` を変えるのも `wheel` のリスナーとスワイプだけです。

W3Cはこれを失敗例F54としています。ポインティングデバイス固有のイベントハンドラーだけで機能を実行している、という失敗です。達成基準は特定のキーを指定していません。同じことをキーボードでできる方法を求めているので、下の修正の1つのように、各セクションを選ぶボタンがあるページなら満たせます。このページには、途中のセクションへキーボードで進む方法がまったくありません。

Tabでは解決しません。文章だけのセクションにはフォーカスできるものがないので、Tabは止まれません。リンクに届いたときは、ブラウザーがリンクを見せようとして固定された `body` をスクロールします。スクリプトはこのスクロールを把握していないので、次にホイールを回すと、スクリプトが数えているセクションと画面がずれてしまいます。

## 利用者への影響

マウスなら動き、スワイプを扱っていればスマートフォンでも動くため、誰もキーを試さないまま公開されることがあります。

## 自動チェックとAIの限界

マークアップは正しく、どのセクションの文章もページの中にあるため、axeなどのマークアップのチェックは通ります。EaseWebの監査はキーを実際に押します。ページにフォーカスがある状態でPage Down、Space、下矢印キー、Endを押し、次にTabを押してからもう一度Page Downを押し、最後にホイールを回します。ホイールでは動くのにどのキーでも動かないページは、`ew:scroll-keys-manual-check` として報告します。キーを押し続けても、ページの終わりより手前でスクロールが止まるページも同じです。これはレビュー用の手がかりで、問題の確定ではありません。ページのどこかにあるボタンで全セクションへ進める場合があり、コードではそこまで判断できないからです。AIによる修正では、矢印キーだけのリスナーを足して、Space、Page Down、Home、Endは効かないままになったり、セクションに `tabindex="0"` を付けてTabの止まる場所を増やすだけで、何も動かないままになったりすることがあります。

## 修正前：ホイールで切り替わる全画面セクション

```html
<main class="screens">
  <section>
    <h1>Aster One</h1>
    <p>平らに折りたためるヘッドホン。</p>
  </section>
  <section>
    <h2>1回の充電で30時間</h2>
    <p>10分の充電で、さらに3時間使えます。</p>
  </section>
  <section>
    <h2>直して使い続ける</h2>
    <p>すべての部品を交換でき、修理ガイドは無料です。</p>
  </section>
  <section>
    <h2>$199</h2>
    <p>送料無料で、30日以内なら返品できます。</p>
    <a href="/order">今すぐ注文</a>
  </section>
</main>
```

```css
html,
body {
  height: 100%;
  overflow: hidden;
}
.screens {
  transition: transform 0.6s ease;
}
.screens section {
  box-sizing: border-box;
  height: 100vh;
  padding: 2rem;
}
```

```js
const screens = document.querySelector('.screens');
let current = 0;
let moving = false;

function show(index) {
  current = Math.min(Math.max(index, 0), screens.children.length - 1);
  screens.style.transform = `translateY(${current * -100}vh)`;
}

window.addEventListener('wheel', (event) => {
  event.preventDefault();
  if (moving) return;
  moving = true;
  setTimeout(() => (moving = false), 700);
  show(current + Math.sign(event.deltaY));
}, { passive: false });
// スワイプでも同じようにshow()を呼びます。
```

- `html` と `body` に `overflow: hidden` を指定しているのでブラウザーにはスクロールできるものがなく、Space、Page Down、矢印キー、Home、Endでは何も動きません。
- `show()` を呼ぶのはホイールとスワイプだけで、キーからは呼ばれません。
- Tabは、フォーカスできるもののない2つのセクションを飛ばして「今すぐ注文」へ直接移ります。
- そのときブラウザーがリンクを見せようとして固定された `body` をスクロールするため、スクリプトが数えているセクションと画面が合わなくなります。

## 修正後：ブラウザーがスクロールし、セクションの位置で止まる

```html
<main class="screens">
  <section>
    <h1>Aster One</h1>
    <p>平らに折りたためるヘッドホン。</p>
  </section>
  <section>
    <h2>1回の充電で30時間</h2>
    <p>10分の充電で、さらに3時間使えます。</p>
  </section>
  <section>
    <h2>直して使い続ける</h2>
    <p>すべての部品を交換でき、修理ガイドは無料です。</p>
  </section>
  <section>
    <h2>$199</h2>
    <p>送料無料で、30日以内なら返品できます。</p>
    <a href="/order">今すぐ注文</a>
  </section>
</main>
```

```css
html {
  scroll-snap-type: y proximity;
}
.screens section {
  box-sizing: border-box;
  min-height: 100vh;
  padding: 2rem;
  scroll-snap-align: start;
}
```

1. `overflow: hidden` とスクリプトを外します。ページがまたスクロールするようになり、何も書かなくても、Space、Page Down、矢印キー、Home、Endで動き、Tabでも各リンクが画面に入ります。
2. `html` に `scroll-snap-type: y proximity` を、各セクションに `scroll-snap-align: start` を指定します。SpaceとPage Downで、ホイールのときと同じように次のセクションの位置で止まります。
3. `height` を `min-height` に変えます。拡大時や小さな画面で高さが足りなくなったセクションは伸び、その中をキーでスクロールできます。

これで修正前の例は直り、スクリプトなしで2.1.1を満たします。セクションが現れるときのアニメーションは、スクロール位置に連動させれば残せます。キーの処理はブラウザーに任せたままです。

## 修正後：スクリプトを残し、キーとフォーカスにも対応する

```html
<main class="screens">
  <section>
    <h1>Aster One</h1>
    <p>平らに折りたためるヘッドホン。</p>
  </section>
  <section>
    <h2>1回の充電で30時間</h2>
    <p>10分の充電で、さらに3時間使えます。</p>
  </section>
  <section>
    <h2>直して使い続ける</h2>
    <p>すべての部品を交換でき、修理ガイドは無料です。</p>
  </section>
  <section>
    <h2>$199</h2>
    <p>送料無料で、30日以内なら返品できます。</p>
    <a href="/order">今すぐ注文</a>
  </section>
</main>
```

```css
html {
  height: 100%;
  overflow: hidden;
}
body {
  height: 100%;
  margin: 0;
  overflow: clip;
}
@media (prefers-reduced-motion: no-preference) {
  .screens {
    transition: transform 0.6s ease;
  }
}
.screens section {
  box-sizing: border-box;
  height: 100vh;
  padding: 2rem;
}
```

```js
const screens = document.querySelector('.screens');
let current = 0;
let moving = false;

function show(index) {
  current = Math.min(Math.max(index, 0), screens.children.length - 1);
  screens.style.transform = `translateY(${current * -100}vh)`;
}

window.addEventListener('wheel', (event) => {
  event.preventDefault();
  if (moving) return;
  moving = true;
  setTimeout(() => (moving = false), 700);
  show(current + Math.sign(event.deltaY));
}, { passive: false });
// スワイプでも同じようにshow()を呼びます。

const STEPS = { PageDown: 1, ArrowDown: 1, ' ': 1, PageUp: -1, ArrowUp: -1 };

window.addEventListener('keydown', (event) => {
  if (event.altKey || event.ctrlKey || event.metaKey) return;
  if (event.target.closest('input, textarea, select, button, summary, [contenteditable]')) return;
  if (event.key === 'Home') show(0);
  else if (event.key === 'End') show(screens.children.length - 1);
  else if (event.key === ' ' && event.shiftKey) show(current - 1);
  else if (event.key in STEPS) show(current + STEPS[event.key]);
  else return;
  event.preventDefault();
});

screens.addEventListener('focusin', (event) => {
  show([...screens.children].indexOf(event.target.closest('section')));
});
```

1. `keydown` のリスナーを足し、Space、Shift + Space、Page Up、Page Down、上下の矢印キー、Home、Endで、ホイールと同じ `show()` を呼びます。
2. テキスト欄、セレクトボックス、ボタンなど、それらのキーを自分で使う要素では何もしないようにします。Ctrl + Endのような組み合わせも同じです。
3. `focusin` のリスナーを足し、フォーカスのあるコントロールを含むセクションを表示します。これで、TabでもShift + Tabでも、フォーカスが画面の外に残りません。
4. `body` の `overflow` を `clip` に変えます。どんな方法でもスクロールできなくなるので、リンクがフォーカスを受けたときに、スクリプトの知らないところでブラウザーがページをスクロールすることがなくなります。

これで修正前の例は直り、2.1.1を満たします。ただし条件が1つあり、どのセクションもウィンドウに収まっていなければなりません。スクリプトは1画面ずつ動かすので、拡大時や小さな画面でウィンドウより高くなったセクションは、切れるか次のセクションに重なり、その中をキーでスクロールすることもできません。このようなスクリプトを残すのは、たいてい次のいずれかのためです。

- セクションの切り替えアニメーション：`show()` から始めれば、キーでもホイールと同じように動きます。`prefers-reduced-motion: no-preference` の中に書いてください。
- 最後のセクションから最初へ戻る動き：キーでも同じようにできますが、HomeとEndは両端へ移動させてください。
- ライブラリーの処理：このリスナーを書くより、ライブラリーのキーボードのオプションを有効にしてください。ただし、SpaceやHome、Endに対応しているとは限らないので、手で確認してください。

## 修正後：セクションごとの本物のボタン

```html
<main class="screens">
  <section>
    <h1>Aster One</h1>
    <p>平らに折りたためるヘッドホン。</p>
  </section>
  <section>
    <h2>1回の充電で30時間</h2>
    <p>10分の充電で、さらに3時間使えます。</p>
  </section>
  <section>
    <h2>直して使い続ける</h2>
    <p>すべての部品を交換でき、修理ガイドは無料です。</p>
  </section>
  <section>
    <h2>$199</h2>
    <p>送料無料で、30日以内なら返品できます。</p>
    <a href="/order">今すぐ注文</a>
  </section>
</main>
<nav class="screen-nav" aria-label="画面">
  <button type="button" aria-current="true">Aster One</button>
  <button type="button">30時間</button>
  <button type="button">直して使う</button>
  <button type="button">$199</button>
</nav>
```

```css
html {
  height: 100%;
  overflow: hidden;
}
body {
  height: 100%;
  margin: 0;
  overflow: clip;
}
@media (prefers-reduced-motion: no-preference) {
  .screens {
    transition: transform 0.6s ease;
  }
}
.screens section {
  box-sizing: border-box;
  height: 100vh;
  padding: 2rem;
}
.screen-nav {
  position: fixed;
  inset: auto 1rem 1rem;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}
```

```js
const screens = document.querySelector('.screens');
const buttons = [...document.querySelectorAll('.screen-nav button')];
let current = 0;
let moving = false;

function show(index) {
  current = Math.min(Math.max(index, 0), screens.children.length - 1);
  screens.style.transform = `translateY(${current * -100}vh)`;
  buttons.forEach((button, i) => {
    if (i === current) button.setAttribute('aria-current', 'true');
    else button.removeAttribute('aria-current');
  });
}

window.addEventListener('wheel', (event) => {
  event.preventDefault();
  if (moving) return;
  moving = true;
  setTimeout(() => (moving = false), 700);
  show(current + Math.sign(event.deltaY));
}, { passive: false });
// スワイプでも同じようにshow()を呼びます。

buttons.forEach((button, i) => {
  button.addEventListener('click', () => show(i));
});

screens.addEventListener('focusin', (event) => {
  show([...screens.children].indexOf(event.target.closest('section')));
});
```

1. `nav` を足し、セクションごとの `button` から `show()` を呼びます。Tabでどのボタンにも届き、EnterかSpaceでそのセクションを表示できます。
2. 表示中のセクションのボタンに `aria-current` を付け、セクションが変わったら付け替えます。これで、見た目だけでなく状態も読み取れます。
3. 前の修正と同じ `focusin` のリスナーと `overflow: clip` を足します。Tabで「今すぐ注文」へ移ったときも、そのセクションが表示されます。

これで2.1.1については修正前の例が直り、どのセクションにもキーボードで進めます。ただしSpace、Page Down、矢印キーでは何も動かないままなので、それらのキーを押す人は、ボタンの存在に自分で気付く必要があります。ウィンドウより高いセクションの問題も、前の修正と同じです。このようなページはセクションのドットをすでに描いていることが多く、その場合はこれが最小の変更になります。ドットのままにするなら、それぞれ `button` にして、名前と見えるフォーカスの枠を付けてください。

## どの方法を選ぶか

3つとも2.1.1を満たしますが、条件なしで満たすのは最初の方法だけなので、これを選んでください。ブラウザー自身のスクロールは、スクロールのキー、Tab、タッチのすべてに対応し、保守するスクリプトがなく、拡大されればセクションも伸びます。その代わり、細かな制御はできなくなります。2つのセクションの間で止まることがあり、動きもブラウザーまかせで、決まった時間で動くスライドにはなりません。そのスライドがページの要であるときだけスクリプトを残し、2番目の方法でキーに対応してください。ボタンは、すぐには作り直せないページの応急処置として、または他の2つに添えるものとして考えてください。

## 実装の判断

ここで確認したスクロールのライブラリーは、多くがこの問題を起こしていません。書き直す前に、使っているものを確認してください。2026年11月に、Chromium、Firefox、WebKitで、既定の設定のまま確認した結果は次のとおりです。

- fullPage.js 4：`keyboardScrolling` は既定で有効です。Space、Page Down、矢印キー、Home、Endでセクションが切り替わり、ウィンドウより高いセクションはまずその中がスクロールします。無効にしないでください。
- Lenis 1.3、Locomotive Scroll 5、GSAPのScrollSmoother：ホイールの動きをなめらかにするだけで、キーの処理はブラウザーに任せているので、キーで動きます。Locomotive Scroll 4のsmoothモードはキーを自分で処理していて、3つのエンジンともキーでページが動きました。
- GSAPのObserver：wheel、touch、pointerのイベントを受け取り、キーは受け取りません。そのためドキュメントにあるセクションのデモのように、これで作ったページはこのガイドの修正前の例と同じです。このデモは他のセクションを `visibility: hidden` で隠すので、Tabも届きません。
- Swiper 14で `direction: 'vertical'` と `mousewheel: true` を指定した場合：キーボードのオプションは既定で無効なので、どのキーでも動きません。`keyboard: { enabled: true }` で矢印キー、Page Up、Page Downは動きますが、Space、Home、Endは動きません。クリックできるページネーションの丸がTabで止まり、Enterで切り替えられるのは、A11yモジュールを読み込んでいるときだけです。フルバンドルには含まれていますが、モジュールを個別に読み込むなら追加が必要です。

固定（ピン留め）したセクションでも同じ失敗が起こります。固定そのものは問題ありません。高さのある親の中で `position: sticky` にしたセクションや、GSAPのScrollTriggerで `pin` と `scrub` を指定したセクションはページのスクロール位置に従うので、Page Downで各段階を進めます。問題になるのは、スクリプトがホイールのイベントで段階を進め、それ以外のスクロールを固定した位置へ強制的に戻す場合です。GSAPのフォーラムの回答で、慣性スクロールを止めるために使われている方法です。これを再現して3つのエンジンで動かすと、ページはそのセクションまでスクロールしたあと、上にも下にもキーでは動かなくなり、ホイールでしか先へ進めませんでした。段階はスクロール位置から決めてください。

スクロールスナップ自体は原因ではありません。ウィンドウより高いセクションは、`scroll-snap-type: y mandatory` でも読めます。CSSの仕様は、ウィンドウより大きいスナップ領域の中では自由にスクロールしてよいとしていて、3つのエンジンとも矢印キーでスクロールできました。Page DownとSpaceもChromiumとWebKitでは同じでしたが、Firefoxでは高いセクションの先頭から次のセクションへ飛びました。そのため、3つとも中を順に進めた `proximity` を使ってください。`mandatory` では、長いセクションの中の見出しにスナップさせないでください。仕様は、間の内容にアクセスできなくなることがあると警告しており、3つのエンジンでの確認でも、キーでもホイールでもその内容の位置では止まれませんでした。

**続きがあることを示します。** これを求める達成基準はなく、2.1.1を満たすかどうかには関わりませんが、ウィンドウいっぱいのセクションは、それで完結しているように見えます。Nielsen Norman Groupはこれを「偽の床」（false floor）と呼び、大きなヒーロー画像にはスクロールを促す手がかりがないと指摘しています。同社が勧めるのは、次の内容を画面の下端に少し見せることで、セクションをウィンドウより少し低くすれば、そうなります。スクロールバーも手がかりになりますが、スクロールを始めるまで隠すシステムもあります。下向きの矢印も、スクロールする前から見えていれば同じ役割を果たします。次のセクションへの本物のリンクかボタンにすれば、キーボードでも先へ進めます。セクションのボタンを付ける修正では、ボタンそのものが続きを示します。

## 修正の確認

1. マウスを使わずにページを開き、Space、Page Down、下矢印キー、Page Up、Home、Endを押します。それぞれ別のセクションが表示されます。ボタンの修正ではキーは動かないので、代わりにTabで各ボタンへ移ってEnterを押します。それぞれのセクションが表示されます。
2. Safariでは「Tabキーを押したときにウェブページ上の各項目を強調表示」を有効にして、Tabで「今すぐ注文」へ移り、Shift + Tabで戻ります。フォーカスのあるセクションが毎回画面に表示され、そのあとホイールを回すと、古いセクションからではなく、そのセクションから次へ進みます。
3. ウィンドウの高さを低くするか、ページを200%に拡大して、いちばん長いセクションの終わりまでキーボードで表示します。スクリプトを残す2つの修正では、これが満たすための条件です。文章が切れるなら、そのページにはその修正では足りません。
4. スクリーンリーダーで先頭から読みます。各セクションの見出しと文章が順に読まれ、「今すぐ注文」に届きます。
5. ページを読み込み、何も操作せずに最初の画面を見ます。次のセクションが下の端に見えている、スクロールバーが表示されたままになっている、矢印があるなど、続きがあることが分かるはずです。これを求める達成基準はないので、2.1.1とは分けて記録します。

OS、ブラウザー、支援技術のバージョン、ビルド、日付、各手順の実際の結果を記録してください。ページのセクションを変更したときや、セクションを動かすライブラリーを更新したときは、もう一度確認してください。

## 制限

このサンプルは、どのアプリケーションでも同じ結果になることを保証するものではありません。ここで扱うのは、ページ自身のスクロールをスクリプトが奪っている場合です。ページと同じ大きさで `overflow-y: scroll` を指定した枠は別の問題で、フォーカスが中に入るまでキーが効きません。[キーボードでスクロールできないスクロール領域](/ja/learn/scrollable-regions)を参照してください。スクロールすると続きを読み込む長いリストは[Tabで近づくとフッターが遠ざかる無限スクロール](/ja/learn/infinite-scroll)で、止められない動きは[動きを減らす設定を無視するアニメーション](/ja/learn/reduced-motion)で、バーの下に隠れるフォーカスは[フォーカス中のリンクを隠す固定ヘッダーとバナー](/ja/learn/focus-not-obscured)で扱います。スクリプトがページを止めている間に画面外のリンクがフォーカスを受けると、[フォーカス表示が見えない独自デザインのボタン](/ja/learn/visible-focus)と同じくフォーカスが見えなくなりますが、スクロールを直せば解消します。ライブラリーの確認は、デスクトップ版のChromium、Firefox、WebKitでキーボードを使って行いました。Safariそのもの、スマートフォン、スクリーンリーダーでは確認していません。

---

このガイドをコーディングエージェントで使う: MCP https://easeweb.dev/mcp（設定は https://easeweb.dev/ja/services/developers）
