セッションタイムアウト警告(Session Timeout)— カウントダウンモーダル

通知・オーバーレイ 初級

このコンポーネントについて

セッションタイムアウト警告(session timeout warning)は、業務アプリで一定時間操作がないユーザーに対し、自動ログアウト前に「あとN秒でログアウトします」と知らせるカウントダウンモーダルです。銀行系・社内システムなど、セキュリティ要件の厳しい業務アプリで定番のUIパターンです。このページでは、clickkeydown による無操作を検知してモーダルを表示し、[ログインを延長][今すぐログアウト]を選ばせる一連のフローを、実際に触って確認できるデモとして提供します。

  • 無操作タイマー — クリック・キー入力を「操作」とみなし、一定時間これらが発生しないと警告モーダルを表示する
  • カウントダウン表示 — 「あとN秒でログアウトします」を1秒ごとに更新する
  • 終了時刻ベースのカウントダウンsetInterval の単純な減算ではなく、警告終了時刻との差分計算で残り秒数を求めるため、バックグラウンドタブでタイマーが間引かれてもズレない
  • 延長 / ログアウトの選択 — [ログインを延長]でタイマーをリセットして通常状態に戻り、[今すぐログアウト]/カウントダウン0でログアウト状態に切り替わる
  • ダミーのログイン画面 — ログアウト後は簡易的な再ログイン画面に切り替わり、業務アプリでの実際の体験に近い形で確認できる

実装のポイント・注意点

カウントダウンは setInterval の単純な減算で実装しないでください。バックグラウンドタブではブラウザがタイマーを間引くため、減算方式だと復帰時に「あと-3秒」のような不整合が起こります。警告終了予定時刻(Date.now() + 猶予時間)を保持しておき、毎回その時刻との差分から残り秒数を計算する方式にすることで、タブが非アクティブな間もズレずに復元できます。

「操作」とみなすイベントは clickkeydown のみに絞っています。mousemove を含めると、カーソルが画面内を通過しただけでタイマーがリセットされてしまい、意図せず警告が出なくなるケースが増えるためです。また、警告モーダル表示中は操作イベントによる自動リセットを行いません。ユーザーが「延長する」という意思表示(ボタンクリック)をしない限り無操作扱いを継続することで、離席中に偶然マウスが触れて警告が消えてしまう事故を防いでいます。

実務では、フロント側のタイマーはあくまでUXのための表示であり、実際のセッション有効期限はサーバー側(Cookieの有効期限・APIレスポンスの401等)で管理します。フロントのタイマーとサーバーの実際のセッション期限がズレると「まだ大丈夫」と表示されたのに操作でエラーになる、といった不整合が起きるため、サーバー側の期限とできるだけ近い値をフロントの猶予時間に設定してください。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

ここをクリックまたはキーを入力すると「操作」とみなされ、タイマーがリセットされます。

無操作の時間を計測中です

※ デモでは無操作10秒で警告表示・カウントダウン10秒に短縮しています。実務では警告表示まで5〜30分が一般的です。

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>セッションタイムアウト警告 サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="stm-demo">
  <!-- 通常状態のダミー画面 -->
  <div class="stm-app-view" id="stm-app-view">
    <p class="stm-hint">ここをクリックまたはキーを入力すると「操作」とみなされ、タイマーがリセットされます。</p>
    <textarea class="stm-dummy-input" placeholder="ダミーの入力欄(クリック・入力でタイマーをリセット)"></textarea>
    <p class="stm-status" id="stm-status">無操作の時間を計測中です</p>
  </div>

  <!-- ログアウト後のダミー画面(hidden で非表示) -->
  <div class="stm-login-view" id="stm-login-view" hidden>
    <p class="stm-login-message">セッションがタイムアウトしました。再度ログインしてください。</p>
    <button class="stm-retry-btn" id="stm-retry-btn" type="button">もう一度試す</button>
  </div>

  <!-- 警告モーダル(hidden で非表示) -->
  <div class="stm-overlay" id="stm-overlay" hidden>
    <div class="stm-dialog" role="alertdialog" aria-modal="true" aria-labelledby="stm-dialog-title">
      <h2 class="stm-dialog-title" id="stm-dialog-title">セッションタイムアウトの警告</h2>
      <p class="stm-dialog-message">
        操作がないため、あと<span class="stm-countdown" id="stm-countdown">10</span>秒でログアウトします。
      </p>
      <div class="stm-dialog-actions">
        <button class="stm-extend-btn" id="stm-extend-btn" type="button">ログインを延長</button>
        <button class="stm-logout-btn" id="stm-logout-btn" type="button">今すぐログアウト</button>
      </div>
    </div>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
:root {
  --stm-primary: #2B7FE8;
  --stm-danger: #EF4444;
  --stm-text: #1a2332;
  --stm-text-muted: #5a6a7a;
  --stm-border: #e2e8f0;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family: sans-serif;
  padding: 24px;
  color: var(--stm-text);
}

.stm-demo {
  max-width: 480px;
  margin: 0 auto;
  position: relative;
}

/* ===== 通常状態のダミー画面 ===== */
.stm-app-view {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.stm-hint {
  margin: 0;
  font-size: 13px;
  color: var(--stm-text-muted);
}

.stm-dummy-input {
  width: 100%;
  min-height: 80px;
  padding: 10px 12px;
  font-size: 14px;
  font-family: inherit;
  border: 1.5px solid var(--stm-border);
  border-radius: 6px;
  resize: vertical;
}

.stm-status {
  margin: 0;
  font-size: 12px;
  color: var(--stm-text-muted);
}

/* hidden 属性で非表示にする要素は display: none を明示 */
.stm-app-view[hidden] {
  display: none;
}

/* ===== ログアウト後のダミー画面 ===== */
.stm-login-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  padding: 48px 20px;
  text-align: center;
}

.stm-login-view[hidden] {
  display: none;
}

.stm-login-message {
  margin: 0;
  font-size: 14px;
  color: var(--stm-text);
}

.stm-retry-btn {
  padding: 8px 20px;
  font-size: 14px;
  color: #fff;
  background: var(--stm-primary);
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

/* ===== 警告モーダル ===== */
.stm-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  border-radius: 8px;
}

.stm-overlay[hidden] {
  display: none;
}

.stm-dialog {
  background: #fff;
  max-width: 320px;
  width: 90%;
  padding: 24px;
  border-radius: 10px;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
}

.stm-dialog-title {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 700;
}

.stm-dialog-message {
  margin: 0 0 20px;
  font-size: 14px;
  line-height: 1.6;
}

.stm-countdown {
  font-weight: 700;
  color: var(--stm-danger);
  font-size: 18px;
}

.stm-dialog-actions {
  display: flex;
  gap: 8px;
}

.stm-extend-btn {
  flex: 1;
  padding: 8px 12px;
  font-size: 13px;
  color: #fff;
  background: var(--stm-primary);
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

.stm-logout-btn {
  flex: 1;
  padding: 8px 12px;
  font-size: 13px;
  color: var(--stm-text-muted);
  background: #fff;
  border: 1.5px solid var(--stm-border);
  border-radius: 6px;
  cursor: pointer;
}
// タイマー設定(デモ用に短縮。実務では警告表示まで5〜30分が一般的)
var IDLE_LIMIT_MS = 10000;       // 無操作10秒で警告モーダルを表示
var WARNING_DURATION_MS = 10000; // 警告モーダル表示から10秒でログアウト
var TICK_INTERVAL_MS = 250;      // 監視インターバル

// 状態: 'active'(通常) / 'warning'(警告中) / 'loggedOut'(ログアウト後)
var state = 'active';
var lastActivityTime = Date.now();
var warningEndTime = null;
var tickTimer = null;

var appView = document.getElementById('stm-app-view');
var loginView = document.getElementById('stm-login-view');
var overlay = document.getElementById('stm-overlay');
var countdownEl = document.getElementById('stm-countdown');
var statusEl = document.getElementById('stm-status');
var extendBtn = document.getElementById('stm-extend-btn');
var logoutBtn = document.getElementById('stm-logout-btn');
var retryBtn = document.getElementById('stm-retry-btn');

// 警告モーダルを表示し、終了予定時刻を記録する
function enterWarning() {
  state = 'warning';
  warningEndTime = Date.now() + WARNING_DURATION_MS;
  overlay.hidden = false;
  extendBtn.focus();
}

// ログインを延長する(通常状態に戻す)
function extendSession() {
  state = 'active';
  lastActivityTime = Date.now();
  overlay.hidden = true;
}

// ログアウト状態に切り替える
function logout() {
  state = 'loggedOut';
  overlay.hidden = true;
  appView.hidden = true;
  loginView.hidden = false;
}

// 毎ティック呼ばれる監視処理
function tick() {
  if (state === 'active') {
    if (Date.now() - lastActivityTime >= IDLE_LIMIT_MS) {
      enterWarning();
    }
  } else if (state === 'warning') {
    // 残り秒数は「終了予定時刻との差分」で毎回計算する(setIntervalの減算にしない)
    var remainingMs = warningEndTime - Date.now();
    var remainingSec = Math.max(0, Math.ceil(remainingMs / 1000));
    countdownEl.textContent = remainingSec;
    if (remainingMs <= 0) {
      logout();
    }
  }
}

// 「操作」とみなすイベント(クリック・キー入力のみ。mousemoveは含めない)
function handleActivity() {
  if (state === 'active') {
    lastActivityTime = Date.now();
  }
}

// デモを初期状態に戻す
function resetDemo() {
  state = 'active';
  lastActivityTime = Date.now();
  overlay.hidden = true;
  appView.hidden = false;
  loginView.hidden = true;
  countdownEl.textContent = Math.ceil(WARNING_DURATION_MS / 1000);
}

document.addEventListener('click', handleActivity);
document.addEventListener('keydown', handleActivity);
extendBtn.addEventListener('click', extendSession);
logoutBtn.addEventListener('click', logout);
retryBtn.addEventListener('click', resetDemo);

tickTimer = setInterval(tick, TICK_INTERVAL_MS);
resetDemo();

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。タイマーの秒数変更や多言語対応など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# セッションタイムアウト警告モーダル 作成依頼

## 概要
無操作が一定時間続くと「あとN秒でログアウトします」というカウントダウンモーダルを表示し、
ユーザーに[ログインを延長][今すぐログアウト]を選ばせるUIを実装してください。

## 要件
- クリックまたはキー入力を「操作」とみなし、一定時間(例: 10秒)これらが発生しなければ警告モーダルを表示する
- 警告モーダルには「あとN秒でログアウトします」というカウントダウン表示を入れ、1秒ごとに数字を更新する
- カウントダウンは setInterval の単純な減算ではなく、警告終了予定時刻(Date.now() + 猶予時間)との差分計算で残り秒数を求める(バックグラウンドタブでのタイマー間引きによるズレを防ぐため)
- [ログインを延長]ボタンでモーダルを閉じ、無操作タイマーをリセットする
- [今すぐログアウト]ボタン、またはカウントダウンが0になった場合は、ログアウト後の画面(再ログインを促すメッセージ+やり直しボタン)に切り替える
- 警告モーダル表示中は、クリック・キー入力だけでは無操作タイマーをリセットしない(延長ボタンを押した場合のみリセットする)

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
無操作の状態が続くと警告モーダルが表示され、モーダル内でカウントダウンが進みます。
カウントダウンが0になると自動的にログアウト状態に切り替わり、再ログインを促す画面が表示されます。
モーダル表示中に[ログインを延長]を押すとモーダルが閉じ、通常状態に戻ります。
状態は「通常」「警告中」「ログアウト後」の3状態で管理し、状態に応じて表示を切り替えてください。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。