「決済完了ページで戻るボタンを押されると二重決済になる」「フォーム送信後に戻られると状態がおかしくなる」など、ナビゲーションを制御したい場面があります。JavaScriptで戻るボタンを「無効化」する方法としてよく紹介される手法が history.pushState() + popstate イベントの組み合わせです。
ただしブラウザの戻るボタンを JavaScript で完全に無効化することはできません。この記事ではよく使われる実装パターン・その仕組み・限界・本当にやりたいことに応じた正しい代替手段まで整理します。
なぜ戻るボタンを完全に無効化できないのか
ブラウザのナビゲーション(戻る・進む)はセキュリティとユーザビリティの観点から、Webページ側のJavaScriptが直接制御できないように設計されています。
history.pushState() や history.replaceState() で URL を書き換えることはできますが、ブラウザの「戻るボタン」そのものを disabled にする API は存在しません。悪意あるサイトがユーザーを閉じ込める手段になってしまうため、仕様として意図的に制限されています。JavaScript でできるのは「戻ったことを検知して、再度現在のページの履歴をスタックに積む」ことだけです。| できること | できないこと |
|---|---|
戻ったことを popstate で検知する |
戻るボタンを UI から消す |
| 戻ったとき現在ページに留まらせる(疑似無効化) | ブラウザの履歴スタック自体を操作・消去する |
| ページ離脱前に確認ダイアログを表示する | 確認なしにナビゲーションを強制キャンセルする |
| URL を書き換えて履歴を追加・置換する | 既存の履歴エントリを削除する |
pushState + popstate による疑似無効化の仕組み
「戻るボタンを押しても前のページに移動しない」ように見せる手法として、history.pushState() で現在ページの URL を履歴に積み続ける方法があります。
history.pushState(null, "", location.href)でページ読み込み時に現在 URL を履歴に追加する- 戻るボタンが押されると
popstateイベントが発火する(履歴を1つ戻る動作) popstateハンドラ内で再度pushState()を呼んで「戻った先」に現在 URL を積み直す- 結果として、戻るボタンを何回押しても常に現在ページの URL に戻ってくる
// ページ読み込み時に現在 URL を履歴スタックに追加
history.pushState(null, '', location.href);
// 戻るボタンが押されたとき(= popstate 発火時)
window.addEventListener('popstate', () => {
// 再度 pushState して「疑似的に元のページに戻す」
history.pushState(null, '', location.href);
});
戻るボタンを押したとき、一瞬前のページが表示されてから現在ページに戻る動作になることがあります。完全なブロックではなく、戻る → 即座に戻り先に現在ページを再挿入するという動作です。ブラウザや OS によっては履歴が蓄積されてボタンが有効のまま残る場合もあります。
// <head> 内や body 上部に記述するなら DOMContentLoaded を使う
document.addEventListener('DOMContentLoaded', () => {
history.pushState(null, '', location.href);
window.addEventListener('popstate', () => {
history.pushState(null, '', location.href);
});
});
replaceState との使い分け
pushState と replaceState は似ていますが、履歴への追加方法が異なります。
// pushState: 現在の位置に「新しい」履歴エントリを追加する
// 戻るボタンで一つ前の状態に戻れる(履歴が増える)
history.pushState({ page: 1 }, '', '/step1');
// replaceState: 現在の履歴エントリを「置き換える」
// 戻るボタンを押してもここには戻れなくなる(履歴が増えない)
history.replaceState({ page: 2 }, '', '/step2');
| メソッド | 履歴への影響 | 戻るボタンの動作 | 主な用途 |
|---|---|---|---|
pushState() |
エントリを追加 | 一つ前の状態に戻れる | SPA のページ遷移 |
replaceState() |
現エントリを置換 | このページには戻れない | フォーム送信後のリダイレクト相当 |
// フォーム送信後のページで「戻ると再送信される」を防ぐパターン
// (PRG パターンがサーバー側でできない場合の代替)
document.addEventListener('DOMContentLoaded', () => {
// 現在のエントリを置き換えることで「このページ」を履歴から抹消
history.replaceState(null, '', location.href);
// 戻るボタンはフォームページではなくその前のページに飛ぶ
});
フォーム送信後に戻るボタンで再送信される問題は、サーバー側で PRG パターン(Post/Redirect/Get)を実装するのが正しい解決策です。フォーム POST を受け取ったサーバーが処理後に GET リダイレクトを返すことで、戻るボタンを押してもフォームの再送信確認は表示されません。JavaScript の
replaceState はサーバー側の変更が難しいときの代替手段です。beforeunload との使い分け
「戻るボタンを押したとき」に確認ダイアログを表示したい場合は、popstate ではなく beforeunload イベントが適切です。
// フォームが変更されたかどうかのフラグ
let formDirty = false;
document.querySelectorAll('input, textarea, select').forEach(el => {
el.addEventListener('change', () => { formDirty = true; });
});
// ページを離れようとしたとき(戻る・閉じる・URL入力などすべてに反応)
window.addEventListener('beforeunload', (e) => {
if (!formDirty) return; // 変更なければスキップ
// 標準的な書き方(メッセージ内容はブラウザ固定、カスタム不可)
e.preventDefault();
e.returnValue = ''; // Chrome では空文字でも動作
});
// フォーム送信時はフラグをリセット(確認なしで遷移させる)
document.querySelector('form')?.addEventListener('submit', () => {
formDirty = false;
});
| 目的 | 適切な手法 |
|---|---|
| 戻るボタンを完全に無効化(疑似) | pushState + popstate |
| 「戻っても再送信されない」ようにする | replaceState / サーバー側 PRG パターン |
| フォーム未保存で離脱前に確認する | beforeunload イベント |
| 決済完了後に戻られたくない | サーバー側でトークン検証 + replaceState |
実践ユースケース別の実装
/**
* 決済完了ページなど「戻ってほしくないページ」で使う
* replaceState で現在ページを履歴の末端にする
*/
document.addEventListener('DOMContentLoaded', () => {
// このページを現在の履歴エントリとして確定(前のページに「戻れなくなる」)
history.replaceState({ page: 'complete' }, '', location.href);
// 念のため popstate も監視(別タブから遷移してきた場合の担保)
window.addEventListener('popstate', (e) => {
if (e.state?.page === 'complete') return;
// それ以外の履歴に戻ろうとした場合はリダイレクト
location.replace('/'); // トップページへ強制移動
});
});
/**
* マルチステップフォームで pushState を使い
* ブラウザの戻るボタン = 前のステップに戻る、という UX を実現する
*/
class StepForm {
#steps;
#current = 0;
constructor(containerSelector) {
this.#steps = Array.from(document.querySelectorAll(`${containerSelector} .step`));
this.#showStep(0);
// 初期状態をプッシュ
history.pushState({ step: 0 }, '', location.href);
// 戻るボタン = 前のステップへ
window.addEventListener('popstate', (e) => {
const step = e.state?.step ?? 0;
this.#showStep(step, false); // 履歴操作なしで表示だけ変える
});
}
#showStep(index, pushHistory = true) {
this.#steps.forEach((s, i) => {
s.hidden = (i !== index);
});
this.#current = index;
if (pushHistory) {
history.pushState({ step: index }, '', `#step-${index + 1}`);
}
}
next() { if (this.#current < this.#steps.length - 1) this.#showStep(this.#current + 1); }
prev() { history.back(); } // ← ブラウザの戻るを利用する
}
document.addEventListener('DOMContentLoaded', () => {
const form = new StepForm('#checkout-form');
document.getElementById('btn-next')?.addEventListener('click', () => form.next());
});
- ブラウザの戻るボタンが「前のステップに戻る」として自然に機能する
- URL が変わるので各ステップをブックマーク・共有できる
- History API を正しい用途で使うため、ブラウザとの衝突が起きにくい
UX・アクセシビリティ上の注意点
ブラウザの戻るボタンはユーザーが「自分のコントロール」のもとナビゲーションする最も基本的な手段です。正当な理由なく無効化すると、ユーザーを閉じ込めた印象を与え、離脱率や信頼感に悪影響を与える可能性があります。
使うべきケース:決済完了・二重送信防止・シングルページアプリの状態管理
避けるべきケース:広告ページへの誘導・強制的な滞在・通常のコンテンツページ
| ケース | 推奨手法 | 理由 |
|---|---|---|
| 決済完了後に戻させたくない | replaceState + サーバー側の二重処理防止 |
安全かつ UX 的に自然 |
| フォーム送信後のリロード再送信防止 | PRG パターン(サーバー側リダイレクト) | HTTP レベルで正しく解決 |
| 未保存データの離脱警告 | beforeunload |
ブラウザが標準UIで確認を取る |
| ステップフォームの前後移動 | pushState + popstate で自前ルーティング |
戻るボタンを有効活用できる |
| SPA のページ遷移管理 | React Router / Vue Router 等のライブラリ | History API を安全にラップ済み |
よくある質問(FAQ)
popstate が発火しないことがあります。なぜですか?popstate は pushState() / replaceState() で追加した履歴エントリに対してのみ発火します。ページを最初に開いただけでは履歴エントリがないため、pushState(null, "", location.href) で事前に1つ積んでおく必要があります。また、一部のブラウザではページ読み込み直後に popstate が発火する場合があるため、DOMContentLoaded の後でリスナーを登録するのが安全です。popstate のタイミングが他のブラウザと異なる場合があります。また、iOS は「戻る」を CSS でフェードインしながら行うため、pushState による疑似無効化がうまく効かないケースがあります。決済完了後のような重要な場面では JavaScript だけに頼らず、サーバー側でのトークン検証や PRG パターンと組み合わせることを強く推奨します。history.go(-1) や history.back() との違いは?history.back() は history.go(-1) と同じで、JS から意図的に1つ前の履歴に戻る命令です。ユーザーがブラウザの戻るボタンを押したときに発火する popstate イベントとは別物です。history.back() を呼ぶと popstate は発火しますが、popstate リスナーで再度 pushState() を呼ぶと無限ループになる可能性があるため注意してください。ブラウザバック操作とプログラムからのバックは event.isTrusted では区別できないため、state の値で判断するのが安全です。pushState / popstate を内部で使い、「URL の変化 = コンポーネントの切り替え」として安全に管理しています。フレームワークを使っている場合は直接 history.pushState() を呼ぶのは競合の原因になるため避け、ルーターのAPIを使ってください。SPA でのHistory API の使い方全般はURLを取得・操作する方法(pushState・URLSearchParams)で詳しく解説しています。まとめ
| 手法 | できること | できないこと |
|---|---|---|
pushState + popstate |
戻るボタンを押しても留まらせる(疑似無効化) | 戻るボタン自体を無効にする・履歴を消す |
replaceState |
現ページを履歴から「なかったこと」にする | 過去の履歴エントリを削除する |
beforeunload |
離脱前に確認ダイアログを表示する | ダイアログのテキストをカスタマイズする |
「戻る・進む」ボタン自体を JavaScript から操作する方法は戻るボタンと進むボタンを操作する方法を、ページ離脱前の確認ダイアログの詳細実装はページ離脱時にアラートを表示する完全ガイドも参照してください。