トグルスイッチ&セグメント切替

チェックボックスを土台にしたON/OFFスイッチと、インジケーターが動くピル型セグメント。設定画面や表示期間の切替に使えます。

#css#javascript#forms#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:通知設定トグル+請求期間のセグメント切替 -->
<div class="app">
  <header class="app__bar">
    <span class="app__logo">💼 FlowDesk</span>
    <span class="app__tag">アカウント設定</span>
  </header>

  <main class="settings">
    <h1 class="settings__h1">通知</h1>

    <div class="row">
      <div><p class="row__title">メール通知</p><p class="row__sub">タスクの更新をメールで受け取る</p></div>
      <label class="switch"><input type="checkbox" checked><span class="switch__track"></span></label>
    </div>
    <div class="row">
      <div><p class="row__title">デスクトップ通知</p><p class="row__sub">ブラウザにポップアップを表示</p></div>
      <label class="switch"><input type="checkbox"><span class="switch__track"></span></label>
    </div>
    <div class="row">
      <div><p class="row__title">週次レポート</p><p class="row__sub">毎週月曜にサマリーを送信</p></div>
      <label class="switch"><input type="checkbox" checked><span class="switch__track"></span></label>
    </div>

    <h1 class="settings__h1">お支払いプラン</h1>
    <!-- ピル型セグメント:インジケーターが動く -->
    <div class="seg" data-seg>
      <span class="seg__ink" aria-hidden="true"></span>
      <button class="seg__btn is-active" data-plan="月払い">月払い</button>
      <button class="seg__btn" data-plan="年払い">年払い <em>2ヶ月分お得</em></button>
    </div>
    <p class="settings__note">選択中:<strong data-plan-out>月払い</strong></p>
  </main>
</div>
CSS
/* FlowDesk SaaS テーマ */
:root{--navy:#0f1b34;--blue:#4f7cff;--ink:#1d2740;--line:#e3e8f2;--muted:#6b7794;--bg:#f4f6fb}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--ink)}
.app{max-width:480px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column}
.app__bar{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--navy);color:#fff}
.app__logo{font-weight:700}
.app__tag{font-size:.78rem;color:#aab6d6}
.settings{flex:1;padding:18px 22px}
.settings__h1{margin:18px 0 10px;font-size:.78rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.settings__h1:first-child{margin-top:4px}
.row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:13px 0;border-bottom:1px solid var(--line)}
.row__title{margin:0;font-size:.92rem;font-weight:600}
.row__sub{margin:2px 0 0;font-size:.76rem;color:var(--muted)}
/* トグルスイッチ */
.switch{position:relative;flex:none;width:46px;height:26px;cursor:pointer}
.switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
.switch__track{position:absolute;inset:0;border-radius:999px;background:#cdd5e6;transition:background .25s}
.switch__track::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .25s}
.switch input:checked + .switch__track{background:var(--blue)}
.switch input:checked + .switch__track::after{transform:translateX(20px)}
.switch input:focus-visible + .switch__track{outline:2px solid var(--blue);outline-offset:2px}
/* セグメント切替 */
.seg{position:relative;display:flex;background:#e8ecf6;border-radius:12px;padding:4px;margin-bottom:10px}
.seg__ink{position:absolute;top:4px;left:4px;bottom:4px;width:calc(50% - 4px);background:#fff;border-radius:9px;box-shadow:0 2px 8px rgba(15,27,52,.12);transition:transform .28s cubic-bezier(.4,0,.2,1)}
.seg__btn{position:relative;z-index:1;flex:1;appearance:none;background:none;border:none;cursor:pointer;font:inherit;font-weight:600;font-size:.88rem;color:var(--muted);padding:9px 4px;transition:color .25s}
.seg__btn.is-active{color:var(--navy)}
.seg__btn em{font-style:normal;font-size:.66rem;color:var(--blue);margin-left:4px}
.settings__note{margin:0;font-size:.84rem;color:var(--muted)}
.settings__note strong{color:var(--navy)}
@media (prefers-reduced-motion:reduce){.switch__track,.switch__track::after,.seg__ink,.seg__btn{transition:none}}
JavaScript
// セグメント切替:インジケーターを選択ボタン側へスライド
const seg = document.querySelector('[data-seg]');
if (seg) {
  const ink = seg.querySelector('.seg__ink');
  const btns = [...seg.querySelectorAll('.seg__btn')];
  const out = document.querySelector('[data-plan-out]');

  btns.forEach((btn, i) => {
    btn.addEventListener('click', () => {
      btns.forEach((b) => b.classList.toggle('is-active', b === btn));
      // 2分割なので index で 0% / 100% へ移動
      if (ink) ink.style.transform = `translateX(${i * 100}%)`;
      if (out) out.textContent = btn.dataset.plan;
    });
  });
}

コード

HTML
<!-- トグルスイッチ:チェックボックスを土台にした見栄えするON/OFF&セグメント切替 -->
<div class="sw">
  <h2 class="sw__title">設定</h2>

  <!-- ON/OFF スイッチ群 -->
  <ul class="sw__list">
    <li class="sw__row">
      <span>ダークモード</span>
      <label class="toggle">
        <input type="checkbox" checked>
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
    <li class="sw__row">
      <span>プッシュ通知</span>
      <label class="toggle">
        <input type="checkbox">
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
    <li class="sw__row">
      <span>自動同期</span>
      <label class="toggle">
        <input type="checkbox" checked>
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
  </ul>

  <!-- セグメント(ピル型)切替 -->
  <div class="seg" data-seg>
    <span class="seg__ink" aria-hidden="true"></span>
    <button class="seg__btn is-active">日</button>
    <button class="seg__btn">週</button>
    <button class="seg__btn">月</button>
  </div>
</div>
CSS
:root{
  --bg:#0f1422;
  --card:#1a2236;
  --on:#34d399;
  --accent:#6366f1;
  --text:#e6ecf7;
  --muted:#8b97b3;
  --off:#3a455f;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#202c4a,transparent),var(--bg);
}
.sw{
  width:min(400px,100%);
  background:var(--card);border:1px solid #28324c;border-radius:18px;
  padding:24px 22px;
}
.sw__title{margin:0 0 16px;font-size:1.1rem}
.sw__list{list-style:none;margin:0 0 22px;padding:0;display:grid;gap:4px}
.sw__row{
  display:flex;align-items:center;justify-content:space-between;
  padding:11px 4px;font-size:.94rem;
  border-bottom:1px solid #232c44;
}
.sw__row:last-child{border-bottom:none}

/* トグルスイッチ本体 */
.toggle{position:relative;display:inline-flex;cursor:pointer}
.toggle input{position:absolute;opacity:0;width:0;height:0}
.toggle__track{
  width:48px;height:28px;border-radius:999px;
  background:#3a455f;transition:background .3s;
  display:flex;align-items:center;padding:3px;
}
.toggle__thumb{
  width:22px;height:22px;border-radius:50%;background:#fff;
  box-shadow:0 2px 6px rgba(0,0,0,.4);
  transition:transform .3s cubic-bezier(.4,0,.2,1);
}
.toggle input:checked + .toggle__track{background:var(--on)}
.toggle input:checked + .toggle__track .toggle__thumb{transform:translateX(20px)}
.toggle input:focus-visible + .toggle__track{outline:2px solid var(--accent);outline-offset:2px}

/* セグメント切替 */
.seg{
  position:relative;display:flex;gap:2px;
  background:#222b42;border-radius:12px;padding:4px;
}
.seg__ink{
  position:absolute;top:4px;bottom:4px;left:4px;width:0;
  border-radius:9px;background:linear-gradient(135deg,var(--accent),#8b5cf6);
  transition:left .3s cubic-bezier(.4,0,.2,1),width .3s cubic-bezier(.4,0,.2,1);
  z-index:0;
}
.seg__btn{
  position:relative;z-index:1;flex:1;
  border:none;background:none;cursor:pointer;
  font:inherit;font-weight:600;color:var(--muted);
  padding:9px 0;transition:color .25s;
}
.seg__btn.is-active{color:#fff}
.seg__btn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:9px}
@media (prefers-reduced-motion:reduce){
  .toggle__thumb,.seg__ink{transition:none}
}
JavaScript
// セグメント切替:選択ボタンへスライドインジケーターを移動
const seg = document.querySelector('[data-seg]');
if (seg) {
  const btns = [...seg.querySelectorAll('.seg__btn')];
  const ink = seg.querySelector('.seg__ink');

  // インジケーターを対象ボタン位置へ
  const move = (btn) => {
    if (!ink || !btn) return;
    ink.style.left = btn.offsetLeft + 'px';
    ink.style.width = btn.offsetWidth + 'px';
  };

  const select = (btn) => {
    btns.forEach((b) => b.classList.toggle('is-active', b === btn));
    move(btn);
  };

  btns.forEach((btn) => btn.addEventListener('click', () => select(btn)));

  // 初期位置(レイアウト確定後)+リサイズ追従
  const active = seg.querySelector('.seg__btn.is-active') || btns[0];
  requestAnimationFrame(() => move(active));
  window.addEventListener('resize', () => move(seg.querySelector('.seg__btn.is-active')));
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「トグルスイッチ&セグメント切替」の効果を追加してください。

# 追加してほしい効果
トグルスイッチ&セグメント切替(UIコンポーネント)
チェックボックスを土台にしたON/OFFスイッチと、インジケーターが動くピル型セグメント。設定画面や表示期間の切替に使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- トグルスイッチ:チェックボックスを土台にした見栄えするON/OFF&セグメント切替 -->
<div class="sw">
  <h2 class="sw__title">設定</h2>

  <!-- ON/OFF スイッチ群 -->
  <ul class="sw__list">
    <li class="sw__row">
      <span>ダークモード</span>
      <label class="toggle">
        <input type="checkbox" checked>
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
    <li class="sw__row">
      <span>プッシュ通知</span>
      <label class="toggle">
        <input type="checkbox">
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
    <li class="sw__row">
      <span>自動同期</span>
      <label class="toggle">
        <input type="checkbox" checked>
        <span class="toggle__track"><span class="toggle__thumb"></span></span>
      </label>
    </li>
  </ul>

  <!-- セグメント(ピル型)切替 -->
  <div class="seg" data-seg>
    <span class="seg__ink" aria-hidden="true"></span>
    <button class="seg__btn is-active">日</button>
    <button class="seg__btn">週</button>
    <button class="seg__btn">月</button>
  </div>
</div>

【CSS】
:root{
  --bg:#0f1422;
  --card:#1a2236;
  --on:#34d399;
  --accent:#6366f1;
  --text:#e6ecf7;
  --muted:#8b97b3;
  --off:#3a455f;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#202c4a,transparent),var(--bg);
}
.sw{
  width:min(400px,100%);
  background:var(--card);border:1px solid #28324c;border-radius:18px;
  padding:24px 22px;
}
.sw__title{margin:0 0 16px;font-size:1.1rem}
.sw__list{list-style:none;margin:0 0 22px;padding:0;display:grid;gap:4px}
.sw__row{
  display:flex;align-items:center;justify-content:space-between;
  padding:11px 4px;font-size:.94rem;
  border-bottom:1px solid #232c44;
}
.sw__row:last-child{border-bottom:none}

/* トグルスイッチ本体 */
.toggle{position:relative;display:inline-flex;cursor:pointer}
.toggle input{position:absolute;opacity:0;width:0;height:0}
.toggle__track{
  width:48px;height:28px;border-radius:999px;
  background:#3a455f;transition:background .3s;
  display:flex;align-items:center;padding:3px;
}
.toggle__thumb{
  width:22px;height:22px;border-radius:50%;background:#fff;
  box-shadow:0 2px 6px rgba(0,0,0,.4);
  transition:transform .3s cubic-bezier(.4,0,.2,1);
}
.toggle input:checked + .toggle__track{background:var(--on)}
.toggle input:checked + .toggle__track .toggle__thumb{transform:translateX(20px)}
.toggle input:focus-visible + .toggle__track{outline:2px solid var(--accent);outline-offset:2px}

/* セグメント切替 */
.seg{
  position:relative;display:flex;gap:2px;
  background:#222b42;border-radius:12px;padding:4px;
}
.seg__ink{
  position:absolute;top:4px;bottom:4px;left:4px;width:0;
  border-radius:9px;background:linear-gradient(135deg,var(--accent),#8b5cf6);
  transition:left .3s cubic-bezier(.4,0,.2,1),width .3s cubic-bezier(.4,0,.2,1);
  z-index:0;
}
.seg__btn{
  position:relative;z-index:1;flex:1;
  border:none;background:none;cursor:pointer;
  font:inherit;font-weight:600;color:var(--muted);
  padding:9px 0;transition:color .25s;
}
.seg__btn.is-active{color:#fff}
.seg__btn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:9px}
@media (prefers-reduced-motion:reduce){
  .toggle__thumb,.seg__ink{transition:none}
}

【JavaScript】
// セグメント切替:選択ボタンへスライドインジケーターを移動
const seg = document.querySelector('[data-seg]');
if (seg) {
  const btns = [...seg.querySelectorAll('.seg__btn')];
  const ink = seg.querySelector('.seg__ink');

  // インジケーターを対象ボタン位置へ
  const move = (btn) => {
    if (!ink || !btn) return;
    ink.style.left = btn.offsetLeft + 'px';
    ink.style.width = btn.offsetWidth + 'px';
  };

  const select = (btn) => {
    btns.forEach((b) => b.classList.toggle('is-active', b === btn));
    move(btn);
  };

  btns.forEach((btn) => btn.addEventListener('click', () => select(btn)));

  // 初期位置(レイアウト確定後)+リサイズ追従
  const active = seg.querySelector('.seg__btn.is-active') || btns[0];
  requestAnimationFrame(() => move(active));
  window.addEventListener('resize', () => move(seg.querySelector('.seg__btn.is-active')));
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。