トグルスイッチ UI(Toggle Switch)— 設定画面のバリエーション

フォーム入力 中級

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

トグルスイッチ(toggle switch)は、設定をすぐにオン・オフしたい場面で使うUIコンポーネントです。同じトグルでも、設定項目の重要度や画面の密度によって、サイズ・ラベルの置き方・操作できない状態の見せ方を使い分けます。さらに即時保存の設定画面では、切り替え直後に反映する楽観的更新と、保存中・失敗時に状態を戻す処理が必要です。このページでは、設定画面を模した1つのパネル内で、実務で使うトグルスイッチのバリエーションと状態設計を確認・コピペできます。

  • サイズとラベル位置の使い分け — 標準サイズの設定行と、コンパクトなインライン配置を比較する
  • disabledの理由を表示 — グレーアウトだけでなく、利用条件を補足して操作できない理由を伝える
  • 楽観的更新の即時保存 — 切り替え直後に状態を反映し、保存中は対象項目だけを操作不能にする
  • 失敗時のロールバック — 保存に失敗したときは以前の値に戻し、再操作できることを通知する

実装のポイント・注意点

トグルは見た目だけの要素ではなく、<input type="checkbox"> を基に実装します。入力要素を display: none にするとキーボードで到達できなくなるため、視覚的に隠す場合もフォーカス可能な状態を保ちます。即時保存では、変更前の checked 値を保持してから保存処理を開始し、失敗時はその値に戻します。

disabled の項目には利用できない理由をテキストで添えます。保存中の一時的な操作不能と、プラン・権限による恒久的な操作不能を同じ見た目だけで済ませず、状態メッセージと補足文で区別することが大切です。成功・失敗は色だけに頼らず、aria-live のメッセージでも伝えます。

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

デモ

アカウント設定

通知と表示

メール通知更新情報をメールで受け取ります
コンパクト表示

利用条件がある設定

Slack通知Businessプランで利用できます

公開設定

プロフィールを公開公開プロフィールを表示します

サンプルソース

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

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <section class="tg2-settings" aria-labelledby="tg2-title">
    <div class="tg2-heading">
      <h1 id="tg2-title">アカウント設定</h1>
      <p id="tg2-status" class="tg2-status" aria-live="polite"></p>
    </div>
    <div class="tg2-group">
      <p class="tg2-group-title">通知と表示</p>
      <div class="tg2-row">
        <div class="tg2-copy"><span class="tg2-label" id="tg2-email-label">メール通知</span><span class="tg2-help">更新情報をメールで受け取ります</span></div>
        <label class="tg2-switch" for="tg2-email"><input class="tg2-input" id="tg2-email" type="checkbox" role="switch" aria-labelledby="tg2-email-label" checked><span class="tg2-track" aria-hidden="true"><span class="tg2-thumb"></span></span></label>
      </div>
      <div class="tg2-row tg2-row-compact">
        <label class="tg2-switch tg2-switch-compact" for="tg2-compact"><input class="tg2-input" id="tg2-compact" type="checkbox" role="switch" aria-labelledby="tg2-compact-label"><span class="tg2-track" aria-hidden="true"><span class="tg2-thumb"></span></span></label>
        <span class="tg2-label" id="tg2-compact-label">コンパクト表示</span>
      </div>
    </div>
    <div class="tg2-group">
      <p class="tg2-group-title">利用条件がある設定</p>
      <div class="tg2-row tg2-row-disabled">
        <div class="tg2-copy"><span class="tg2-label" id="tg2-slack-label">Slack通知</span><span class="tg2-help" id="tg2-slack-help">Businessプランで利用できます</span></div>
        <label class="tg2-switch" for="tg2-slack"><input class="tg2-input" id="tg2-slack" type="checkbox" role="switch" aria-labelledby="tg2-slack-label" aria-describedby="tg2-slack-help" disabled><span class="tg2-track" aria-hidden="true"><span class="tg2-thumb"></span></span></label>
      </div>
    </div>
    <div class="tg2-group">
      <p class="tg2-group-title">公開設定</p>
      <div class="tg2-row">
        <div class="tg2-copy"><span class="tg2-label" id="tg2-public-label">プロフィールを公開</span><span class="tg2-help">公開プロフィールを表示します</span></div>
        <label class="tg2-switch" for="tg2-public"><input class="tg2-input" id="tg2-public" type="checkbox" role="switch" aria-labelledby="tg2-public-label"><span class="tg2-track" aria-hidden="true"><span class="tg2-thumb"></span></span></label>
      </div>
    </div>
  </section>
  <button class="reset-btn" id="tg2-reset" type="button">リセット</button>
  <script src="./script.js"></script>
</body>
</html>
.tg2-settings { max-width: 560px; overflow: hidden; border: 1px solid #d0d7e0; border-radius: 12px; background: #fff; }
.tg2-heading, .tg2-group { padding: 16px 20px; }
.tg2-heading { border-bottom: 1px solid #d0d7e0; }
.tg2-group + .tg2-group { border-top: 1px solid #d0d7e0; }
.tg2-group-title { margin: 0 0 4px; color: #5a6a7a; font-size: 12px; font-weight: 700; }
.tg2-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 68px; }
.tg2-row + .tg2-row { border-top: 1px solid #edf1f5; }
.tg2-label { display: block; font-weight: 700; }
.tg2-help { display: block; margin-top: 3px; color: #5a6a7a; font-size: 12px; }
.tg2-row-compact { justify-content: flex-start; gap: 10px; min-height: 56px; }
.tg2-row-disabled { opacity: 0.58; }
.tg2-switch { position: relative; display: inline-flex; flex: 0 0 52px; align-items: center; width: 52px; min-height: 44px; cursor: pointer; }
.tg2-input { position: absolute; z-index: 1; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: inherit; }
.tg2-track { position: relative; display: block; width: 52px; height: 28px; border-radius: 999px; background: #aab6c3; transition: background 0.2s; }
.tg2-thumb { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; border-radius: 50%; background: #fff; transition: transform 0.2s; }
.tg2-input:checked + .tg2-track { background: #2b7fe8; }
.tg2-input:checked + .tg2-track .tg2-thumb { transform: translateX(24px); }
.tg2-input:focus-visible + .tg2-track { outline: 3px solid rgba(43, 127, 232, 0.35); outline-offset: 3px; }
.tg2-input:disabled + .tg2-track { background: #d2dae3; }
.tg2-switch:has(.tg2-input:disabled) { cursor: not-allowed; }
.tg2-switch-compact { flex-basis: 44px; width: 44px; }
.tg2-switch-compact .tg2-track { width: 40px; height: 22px; }
.tg2-switch-compact .tg2-thumb { width: 16px; height: 16px; }
.tg2-switch-compact .tg2-input:checked + .tg2-track .tg2-thumb { transform: translateX(18px); }
.tg2-status { min-height: 20px; margin: 8px 0 0; font-size: 13px; font-weight: 700; }
.tg2-status.is-success { color: #16803d; }
.tg2-status.is-error { color: #c24141; }
.tg2-row.is-saving { background: #f7fbff; }
.tg2-row.is-saving .tg2-help::after { content: ' 保存中…'; color: #2b7fe8; }
.reset-btn { margin-top: 12px; padding: 6px 16px; border: 1px solid #d0d7e0; border-radius: 6px; background: #fff; cursor: pointer; }
.reset-btn:disabled { cursor: not-allowed; opacity: 0.55; }
@media (prefers-reduced-motion: reduce) { .tg2-track, .tg2-thumb { transition: none; } }
var initialSettings = { 'tg2-email': true, 'tg2-compact': false, 'tg2-public': false };
var settings = Object.assign({}, initialSettings);
var failedSettingId = 'tg2-public';
var inputs = Array.prototype.slice.call(document.querySelectorAll('.tg2-input:not(:disabled)'));
var statusEl = document.getElementById('tg2-status');
var resetButton = document.getElementById('tg2-reset');
var pendingCount = 0;

function announce(message, type) {
  statusEl.textContent = message;
  statusEl.className = 'tg2-status';
  if (type) statusEl.classList.add('is-' + type);
}

function saveSetting(settingId) {
  return new Promise(function (resolve, reject) {
    window.setTimeout(function () {
      if (settingId === failedSettingId) return reject(new Error('保存に失敗しました'));
      resolve();
    }, 700);
  });
}

inputs.forEach(function (input) {
  input.addEventListener('change', function () {
    var settingId = input.id;
    var previousValue = settings[settingId];
    var nextValue = input.checked;
    var row = input.closest('.tg2-row');
    input.disabled = true;
    row.classList.add('is-saving');
    pendingCount += 1;
    resetButton.disabled = true;
    announce('保存中…');

    saveSetting(settingId).then(function () {
      settings[settingId] = nextValue;
      announce('保存しました', 'success');
    }).catch(function () {
      input.checked = previousValue;
      announce('保存に失敗しました。元の設定に戻しました。もう一度お試しください。', 'error');
    }).finally(function () {
      input.disabled = false;
      row.classList.remove('is-saving');
      pendingCount -= 1;
      resetButton.disabled = pendingCount > 0;
    });
  });
});

function resetDemo() {
  if (pendingCount > 0) return;
  settings = Object.assign({}, initialSettings);
  inputs.forEach(function (input) { input.checked = settings[input.id]; });
  announce('');
}

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のトグルスイッチを生成できます。

ChatGPTやClaudeに渡すと、設定項目や保存先APIなどを変更して使えます。

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

💡 ReactやVueで実装したい場合は、プロンプトの末尾に「Reactを使って実装してください」のように追記してください。

Pattern 1 — サイズとラベル位置

# トグルスイッチのサイズ・ラベル位置バリエーション作成依頼

設定画面で使うトグルスイッチを、標準サイズの設定行とコンパクトなインライン配置の2種類で実装してください。

## 要件
- 標準トグルは左にラベルと補足文、右にトグルを置く
- コンパクトトグルはトグルの右にラベルを置く
- キーボード操作とフォーカス表示に対応する
- HTML / CSS / バニラJavaScript、外部ライブラリなしで実装する

Pattern 2 — disabledと利用条件

# disabled状態のトグルスイッチ作成依頼

「Slack通知」のトグルをネイティブのdisabled属性で操作不能にし、「Businessプランで利用できます」という理由を表示してください。グレーアウトだけに頼らず、aria-describedbyでトグルと理由のテキストを関連付けてください。

Pattern 3 — 楽観的更新と失敗時のロールバック

# 即時保存するトグルスイッチ作成依頼

切り替え直後に見た目を更新し、PromiseとsetTimeoutで700msの擬似保存を行うトグルスイッチを実装してください。保存中は対象のトグルだけをdisabledにします。「プロフィールを公開」は必ず保存失敗とし、以前のchecked値へ戻してaria-live領域に失敗理由を表示してください。動的なメッセージはtextContentで表示してください。