ニューモーフィック・トグル&スイッチ

スライドトグルとセグメントスイッチを備えたニューモーフィズムの設定パネル。状態を文章で反映する設定UIのサンプルです。

#css#neumorphism#toggle#javascript#ui

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura ファンクラブ:通知&楽曲フィルタ設定 -->
<section class="st-panel">
  <header class="st-panel__head">
    <span class="st-panel__icon">🌸</span>
    <div>
      <h2 class="st-panel__title">通知・表示の設定</h2>
      <p class="st-panel__sub">Sakura ファンクラブ</p>
    </div>
  </header>

  <!-- 通知トグル群 -->
  <div class="st-row">
    <span class="st-row__label">ライブ情報のお知らせ</span>
    <button class="st-toggle is-on" type="button" aria-pressed="true"><span class="st-toggle__knob"></span></button>
  </div>
  <div class="st-row">
    <span class="st-row__label">新曲・MV公開通知</span>
    <button class="st-toggle is-on" type="button" aria-pressed="true"><span class="st-toggle__knob"></span></button>
  </div>
  <div class="st-row">
    <span class="st-row__label">メンバーブログ更新</span>
    <button class="st-toggle" type="button" aria-pressed="false"><span class="st-toggle__knob"></span></button>
  </div>

  <!-- 楽曲フィルタ(セグメント) -->
  <p class="st-seglabel">楽曲フィルタ</p>
  <div class="st-seg" id="stSeg">
    <span class="st-seg__glider"></span>
    <button class="st-seg__item is-active" type="button" data-i="0">すべて</button>
    <button class="st-seg__item" type="button" data-i="1">バラード</button>
    <button class="st-seg__item" type="button" data-i="2">アップ</button>
  </div>

  <p class="st-readout" id="stReadout">通知2件ON / 表示:すべての楽曲</p>
</section>
CSS
/* Sakura:桜色ニューモーフィズムの設定パネル */
:root {
  --bg: #fbf0f4;
  --dark: #e7cdd8;
  --light: #ffffff;
  --pink: #ff7aa8;
  --pink-deep: #e85d90;
  --text: #7a5266;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  display: grid;
  place-items: center;
  background: var(--bg);
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  color: var(--text);
  overflow: hidden;
}

/* 凸の設定パネル */
.st-panel {
  width: min(330px, 88vw);
  padding: 22px 22px 18px;
  border-radius: 26px;
  background: var(--bg);
  box-shadow: 9px 9px 18px var(--dark), -9px -9px 18px var(--light);
}

.st-panel__head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; }
.st-panel__icon {
  display: inline-grid;
  place-items: center;
  width: 40px; height: 40px;
  border-radius: 14px;
  font-size: 18px;
  background: var(--bg);
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
}
.st-panel__title { margin: 0; font-size: 15px; font-weight: 800; color: #6a3650; }
.st-panel__sub { margin: 2px 0 0; font-size: 11px; color: #b08aa0; }

.st-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.st-row__label { font-size: 13px; font-weight: 600; color: #6a4658; }

/* スライドトグル:凹みトラック+凸ノブ */
.st-toggle {
  position: relative;
  width: 56px; height: 30px;
  border: none; cursor: pointer; padding: 0;
  border-radius: 999px;
  background: var(--bg);
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
  transition: background 0.25s ease;
}
.st-toggle__knob {
  position: absolute;
  top: 4px; left: 4px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: 2px 2px 5px var(--dark), -2px -2px 5px var(--light);
  transition: transform 0.26s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s ease;
}
/* ON:ノブを右へ+ピンク */
.st-toggle.is-on .st-toggle__knob { transform: translateX(26px); background: var(--pink); }

/* セグメントスイッチ */
.st-seglabel { margin: 18px 0 8px; font-size: 12px; font-weight: 700; color: #9a7185; }
.st-seg {
  position: relative;
  display: flex;
  padding: 5px;
  border-radius: 14px;
  background: var(--bg);
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
}
/* 動く凸インジケータ(JSで translate) */
.st-seg__glider {
  position: absolute;
  top: 5px; left: 5px;
  width: calc((100% - 10px) / 3);
  height: calc(100% - 10px);
  border-radius: 10px;
  background: var(--bg);
  box-shadow: 3px 3px 6px var(--dark), -3px -3px 6px var(--light);
  transform: translateX(0);
  transition: transform 0.3s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.st-seg__item {
  position: relative; z-index: 1;
  flex: 1;
  font: inherit; font-size: 12.5px; font-weight: 700;
  color: #b08aa0;
  border: none; background: none; cursor: pointer;
  padding: 9px 0;
  transition: color 0.25s ease;
}
.st-seg__item.is-active { color: var(--pink-deep); }

.st-readout { margin: 16px 0 2px; font-size: 11.5px; text-align: center; color: #b08aa0; min-height: 1em; }

@media (prefers-reduced-motion: reduce) {
  .st-toggle__knob, .st-seg__glider { transition: none; }
}
JavaScript
// 通知トグルと楽曲フィルタの状態を文章へ反映する
const toggles = document.querySelectorAll(".st-toggle");
const seg = document.getElementById("stSeg");
const readout = document.getElementById("stReadout");

// フィルタの日本語ラベル
const filters = ["すべての楽曲", "バラード", "アップテンポ"];
let filterIndex = 0;

// 状態文を更新(null安全)
function render() {
  const onCount = document.querySelectorAll(".st-toggle.is-on").length;
  if (readout) readout.textContent = `通知${onCount}件ON / 表示:${filters[filterIndex]}`;
}

// スライドトグル:ON/OFF切替
toggles.forEach((tg) => {
  tg.addEventListener("click", () => {
    tg.classList.toggle("is-on");
    tg.setAttribute("aria-pressed", tg.classList.contains("is-on") ? "true" : "false");
    render();
  });
});

// セグメント:グライダー移動+アクティブ切替
if (seg) {
  const items = seg.querySelectorAll(".st-seg__item");
  const glider = seg.querySelector(".st-seg__glider");
  items.forEach((item) => {
    item.addEventListener("click", () => {
      items.forEach((el) => el.classList.remove("is-active"));
      item.classList.add("is-active");
      filterIndex = Number(item.dataset.i) || 0;
      // 3分割幅ぶんスライド
      if (glider) glider.style.transform = `translateX(${filterIndex * 100}%)`;
      render();
    });
  });
}

render(); // 初期表示

実装ガイド

使いどころ

設定画面の ON/OFF スイッチ、表示モードやプラン選択のセグメントコントロールなど、状態を切り替える設定UIに最適です。やわらかいニューモーフィズムで統一した設定パネルやプレファレンス画面に向きます。

実装時の注意点

トグルは凹みトラック(inset シャドウ)+凸ノブ(外向きシャドウ)で構成し、is-on クラスでノブを translateX し accent 色に変えます。セグメントは inset の溝の上を凸の glider が transform で滑る作りで、JS は選択インデックスから glider の移動量を更新するだけです。状態は文章(readout)にも反映してスクリーンリーダー利用者にも伝わるようにします。

対応ブラウザ

box-shadow・inset・transform・transition のみで実装され backdrop-filter は不使用のため、全モダンブラウザで安定動作します。cubic-bezier のオーバーシュートを使ったノブの跳ね返りも標準機能で、プレフィックスや対応バージョンの懸念はありません。

よくある失敗

ニューモーフィズム特有の淡い影だけでは ON/OFF の判別が付きにくく、色の変化(accent)を併用しないと色覚特性のあるユーザーが状態を区別できません。トラックとノブのコントラスト不足で UIコンポーネントの 3:1(WCAG 1.4.11)を満たしにくく、ネイティブの input[type=checkbox] を置き換える場合は role/aria-checked やキーボード操作(Space/Enter)の実装漏れで操作不能になりがちです。

応用例

button に aria-pressed、セグメントに role="tablist"/radiogroup と aria-checked を付けてアクセシブルにする、:focus-visible でフォーカス可視化、状態を localStorage に保存、ダークテーマ用に base と明暗シャドウ色を再定義する、などの拡張で実運用の設定UIへ昇格できます。

コード

HTML
<!-- ニューモーフィックなトグル&セグメントスイッチのコントロールパネル -->
<div class="nt-stage">
  <div class="nt-panel">
    <p class="nt-panel__title">Settings</p>

    <!-- スライド式トグル2種 -->
    <div class="nt-row">
      <span class="nt-row__label">機内モード</span>
      <button class="nt-toggle" id="ntToggle1" role="switch" aria-checked="false" type="button">
        <span class="nt-toggle__knob"></span>
      </button>
    </div>
    <div class="nt-row">
      <span class="nt-row__label">ダークテーマ</span>
      <button class="nt-toggle is-on" id="ntToggle2" role="switch" aria-checked="true" type="button">
        <span class="nt-toggle__knob"></span>
      </button>
    </div>

    <!-- セグメントスイッチ(凹みトラックに凸インジケータ) -->
    <div class="nt-seg" id="ntSeg" role="tablist">
      <span class="nt-seg__glider" id="ntGlider"></span>
      <button class="nt-seg__item is-active" data-seg type="button">日</button>
      <button class="nt-seg__item" data-seg type="button">週</button>
      <button class="nt-seg__item" data-seg type="button">月</button>
    </div>

    <p class="nt-readout" id="ntReadout">日 表示・ダークテーマ ON</p>
  </div>
</div>
CSS
/* ニューモーフィックなコントロール群 */
:root {
  --bg: #e4e8ef;
  --dark: #bcc3d0;
  --light: #ffffff;
  --accent: #4f8cff;
  --text: #5a6377;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  background: var(--bg);
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
}

.nt-stage { padding: 16px; }

/* 凸パネル */
.nt-panel {
  width: min(300px, 84vw);
  padding: 24px 24px 22px;
  border-radius: 24px;
  background: var(--bg);
  box-shadow: 9px 9px 18px var(--dark), -9px -9px 18px var(--light);
}
.nt-panel__title { margin: 0 0 18px; font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; color: #97a0b3; }

.nt-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.nt-row__label { font-size: 14px; font-weight: 600; color: #4b5468; }

/* スライドトグル:凹みトラック+凸ノブ */
.nt-toggle {
  position: relative;
  width: 58px; height: 30px;
  border: none; cursor: pointer; padding: 0;
  border-radius: 999px;
  background: var(--bg);
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
  transition: background 0.25s ease;
}
.nt-toggle__knob {
  position: absolute;
  top: 4px; left: 4px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: 2px 2px 5px var(--dark), -2px -2px 5px var(--light);
  transition: transform 0.26s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s ease;
}
/* ON:ノブを右へ移動+アクセント色 */
.nt-toggle.is-on .nt-toggle__knob { transform: translateX(28px); background: var(--accent); }

/* セグメントスイッチ */
.nt-seg {
  position: relative;
  display: flex;
  margin-top: 6px;
  padding: 5px;
  border-radius: 14px;
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
}
/* 動く凸インジケータ(JSで transform 更新) */
.nt-seg__glider {
  position: absolute;
  top: 5px; left: 5px;
  width: calc((100% - 10px) / 3);
  height: calc(100% - 10px);
  border-radius: 10px;
  background: var(--bg);
  box-shadow: 3px 3px 6px var(--dark), -3px -3px 6px var(--light);
  transform: translateX(0);
  transition: transform 0.3s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.nt-seg__item {
  position: relative; z-index: 1;
  flex: 1;
  font: inherit; font-size: 13px; font-weight: 600;
  color: #8a93a6;
  border: none; background: none; cursor: pointer;
  padding: 9px 0;
  transition: color 0.25s ease;
}
.nt-seg__item.is-active { color: var(--accent); }

.nt-readout { margin: 16px 0 0; font-size: 12px; text-align: center; color: #97a0b3; min-height: 1em; }

@media (prefers-reduced-motion: reduce) {
  .nt-toggle__knob, .nt-seg__glider { transition: none; }
}
JavaScript
// トグルとセグメントスイッチの状態管理+読み上げ表示の更新
const toggles = document.querySelectorAll(".nt-toggle");
const seg = document.getElementById("ntSeg");
const glider = document.getElementById("ntGlider");
const segItems = seg ? seg.querySelectorAll("[data-seg]") : [];
const readout = document.getElementById("ntReadout");

const state = { range: "日", dark: true };

// 現在状態を文章で反映
function render() {
  if (readout) readout.textContent = `${state.range} 表示・ダークテーマ ${state.dark ? "ON" : "OFF"}`;
}

// スライドトグル:クリックで ON/OFF を反転
toggles.forEach((tg) => {
  tg.addEventListener("click", () => {
    const on = tg.classList.toggle("is-on");
    tg.setAttribute("aria-checked", String(on));
    // 2番目(ダークテーマ)のみ読み上げに反映
    if (tg.id === "ntToggle2") { state.dark = on; render(); }
  });
});

// セグメント:選択項目へグライダーを移動
segItems.forEach((item, i) => {
  item.addEventListener("click", () => {
    segItems.forEach((s) => s.classList.remove("is-active"));
    item.classList.add("is-active");
    if (glider) glider.style.transform = `translateX(${i * 100}%)`;
    state.range = item.textContent.trim();
    render();
  });
});

render();

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

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

# 追加してほしい効果
ニューモーフィック・トグル&スイッチ(グラス / ニューモーフィズム)
スライドトグルとセグメントスイッチを備えたニューモーフィズムの設定パネル。状態を文章で反映する設定UIのサンプルです。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ニューモーフィックなトグル&セグメントスイッチのコントロールパネル -->
<div class="nt-stage">
  <div class="nt-panel">
    <p class="nt-panel__title">Settings</p>

    <!-- スライド式トグル2種 -->
    <div class="nt-row">
      <span class="nt-row__label">機内モード</span>
      <button class="nt-toggle" id="ntToggle1" role="switch" aria-checked="false" type="button">
        <span class="nt-toggle__knob"></span>
      </button>
    </div>
    <div class="nt-row">
      <span class="nt-row__label">ダークテーマ</span>
      <button class="nt-toggle is-on" id="ntToggle2" role="switch" aria-checked="true" type="button">
        <span class="nt-toggle__knob"></span>
      </button>
    </div>

    <!-- セグメントスイッチ(凹みトラックに凸インジケータ) -->
    <div class="nt-seg" id="ntSeg" role="tablist">
      <span class="nt-seg__glider" id="ntGlider"></span>
      <button class="nt-seg__item is-active" data-seg type="button">日</button>
      <button class="nt-seg__item" data-seg type="button">週</button>
      <button class="nt-seg__item" data-seg type="button">月</button>
    </div>

    <p class="nt-readout" id="ntReadout">日 表示・ダークテーマ ON</p>
  </div>
</div>

【CSS】
/* ニューモーフィックなコントロール群 */
:root {
  --bg: #e4e8ef;
  --dark: #bcc3d0;
  --light: #ffffff;
  --accent: #4f8cff;
  --text: #5a6377;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 360px;
  display: grid;
  place-items: center;
  background: var(--bg);
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  color: var(--text);
}

.nt-stage { padding: 16px; }

/* 凸パネル */
.nt-panel {
  width: min(300px, 84vw);
  padding: 24px 24px 22px;
  border-radius: 24px;
  background: var(--bg);
  box-shadow: 9px 9px 18px var(--dark), -9px -9px 18px var(--light);
}
.nt-panel__title { margin: 0 0 18px; font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; color: #97a0b3; }

.nt-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.nt-row__label { font-size: 14px; font-weight: 600; color: #4b5468; }

/* スライドトグル:凹みトラック+凸ノブ */
.nt-toggle {
  position: relative;
  width: 58px; height: 30px;
  border: none; cursor: pointer; padding: 0;
  border-radius: 999px;
  background: var(--bg);
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
  transition: background 0.25s ease;
}
.nt-toggle__knob {
  position: absolute;
  top: 4px; left: 4px;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--bg);
  box-shadow: 2px 2px 5px var(--dark), -2px -2px 5px var(--light);
  transition: transform 0.26s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s ease;
}
/* ON:ノブを右へ移動+アクセント色 */
.nt-toggle.is-on .nt-toggle__knob { transform: translateX(28px); background: var(--accent); }

/* セグメントスイッチ */
.nt-seg {
  position: relative;
  display: flex;
  margin-top: 6px;
  padding: 5px;
  border-radius: 14px;
  box-shadow: inset 3px 3px 6px var(--dark), inset -3px -3px 6px var(--light);
}
/* 動く凸インジケータ(JSで transform 更新) */
.nt-seg__glider {
  position: absolute;
  top: 5px; left: 5px;
  width: calc((100% - 10px) / 3);
  height: calc(100% - 10px);
  border-radius: 10px;
  background: var(--bg);
  box-shadow: 3px 3px 6px var(--dark), -3px -3px 6px var(--light);
  transform: translateX(0);
  transition: transform 0.3s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.nt-seg__item {
  position: relative; z-index: 1;
  flex: 1;
  font: inherit; font-size: 13px; font-weight: 600;
  color: #8a93a6;
  border: none; background: none; cursor: pointer;
  padding: 9px 0;
  transition: color 0.25s ease;
}
.nt-seg__item.is-active { color: var(--accent); }

.nt-readout { margin: 16px 0 0; font-size: 12px; text-align: center; color: #97a0b3; min-height: 1em; }

@media (prefers-reduced-motion: reduce) {
  .nt-toggle__knob, .nt-seg__glider { transition: none; }
}

【JavaScript】
// トグルとセグメントスイッチの状態管理+読み上げ表示の更新
const toggles = document.querySelectorAll(".nt-toggle");
const seg = document.getElementById("ntSeg");
const glider = document.getElementById("ntGlider");
const segItems = seg ? seg.querySelectorAll("[data-seg]") : [];
const readout = document.getElementById("ntReadout");

const state = { range: "日", dark: true };

// 現在状態を文章で反映
function render() {
  if (readout) readout.textContent = `${state.range} 表示・ダークテーマ ${state.dark ? "ON" : "OFF"}`;
}

// スライドトグル:クリックで ON/OFF を反転
toggles.forEach((tg) => {
  tg.addEventListener("click", () => {
    const on = tg.classList.toggle("is-on");
    tg.setAttribute("aria-checked", String(on));
    // 2番目(ダークテーマ)のみ読み上げに反映
    if (tg.id === "ntToggle2") { state.dark = on; render(); }
  });
});

// セグメント:選択項目へグライダーを移動
segItems.forEach((item, i) => {
  item.addEventListener("click", () => {
    segItems.forEach((s) => s.classList.remove("is-active"));
    item.classList.add("is-active");
    if (glider) glider.style.transform = `translateX(${i * 100}%)`;
    state.range = item.textContent.trim();
    render();
  });
});

render();

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

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