カスタムチェック / ラジオ

:has() を活用してチェックボックス・カードラジオ・トグルスイッチをCSSだけで装飾。アクセシブルな見た目のリッチな選択UIを構築できます。

#css#form#has-selector

ライブデモ

使用例(お題: カフェ MOON BREW)

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

HTML
<div class="mb-order">
  <header class="mb-order__head">
    <span class="mb-logo">☕ MOON BREW</span>
    <h2 class="mb-order__title">カフェラテ をカスタム</h2>
  </header>

  <form class="mb-order__form" novalidate>
    <p class="cc-label">ミルクを選ぶ</p>
    <!-- カードラジオ -->
    <div class="cc-group" role="radiogroup">
      <label class="cc-opt">
        <input type="radio" name="milk" value="normal" checked>
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>ミルク</strong><small>+¥0</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="milk" value="oat">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>オーツ</strong><small>+¥60</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="milk" value="soy">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>豆乳</strong><small>+¥60</small></span>
      </label>
    </div>

    <p class="cc-label">トッピング</p>
    <!-- カスタムチェック -->
    <label class="cc-check">
      <input type="checkbox" name="shot" checked>
      <span class="cc-box"></span>
      <span>エスプレッソ追加 +¥80</span>
    </label>
    <label class="cc-check">
      <input type="checkbox" name="syrup">
      <span class="cc-box"></span>
      <span>キャラメルシロップ +¥50</span>
    </label>

    <!-- トグルスイッチ -->
    <label class="cc-toggle">
      <span>ホットで提供</span>
      <input type="checkbox" name="hot" checked>
      <span class="cc-switch"></span>
    </label>

    <button class="mb-cart" type="submit">カートに入れる ¥640</button>
  </form>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(120% 100% at 50% 0%, #3a2817, #2b1d12);
  color: #2b1d12;
}

.mb-order {
  width: min(380px, 92vw);
  padding: 22px 24px 24px;
  background: #f5ede1;
  border-radius: 18px;
  box-shadow: 0 24px 56px -22px rgba(0, 0, 0, 0.6);
}
.mb-order__head { margin-bottom: 14px; }
.mb-logo { font-size: 0.78rem; font-weight: 700; letter-spacing: 0.08em; color: #a96e26; }
.mb-order__title { margin: 6px 0 0; font-size: 1.1rem; font-weight: 800; }

.cc-label {
  margin: 14px 0 8px;
  font-size: 0.74rem; font-weight: 700;
  letter-spacing: 0.06em; color: #8a6c45;
}

/* カードラジオ:横並び */
.cc-group { display: flex; gap: 8px; }
.cc-opt {
  flex: 1;
  display: flex; align-items: center; gap: 7px;
  padding: 9px 8px;
  background: #fff;
  border: 2px solid #e3d4ba;
  border-radius: 11px;
  cursor: pointer;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.cc-opt input { position: absolute; opacity: 0; pointer-events: none; }
.cc-radio {
  width: 16px; height: 16px; flex: none;
  border: 2px solid #c7ad86; border-radius: 50%;
  display: grid; place-items: center;
  transition: border-color 0.18s ease;
}
.cc-radio::after {
  content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: #c98a3b; transform: scale(0);
  transition: transform 0.18s ease;
}
.cc-body { display: flex; flex-direction: column; line-height: 1.2; }
.cc-body strong { font-size: 0.82rem; }
.cc-body small { font-size: 0.66rem; color: #9a7e57; }

/* :has() で選択中カードを強調(技法の主役) */
.cc-opt:has(input:checked) { border-color: #c98a3b; box-shadow: 0 6px 14px -8px rgba(201, 138, 59, 0.8); }
.cc-opt:has(input:checked) .cc-radio { border-color: #c98a3b; }
.cc-opt:has(input:checked) .cc-radio::after { transform: scale(1); }

/* カスタムチェック */
.cc-check {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 2px; font-size: 0.85rem; cursor: pointer;
}
.cc-check input { position: absolute; opacity: 0; pointer-events: none; }
.cc-box {
  width: 19px; height: 19px; flex: none;
  border: 2px solid #c7ad86; border-radius: 6px;
  display: grid; place-items: center;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.cc-box::after {
  content: "✓"; color: #fff; font-size: 0.74rem; font-weight: 800;
  transform: scale(0); transition: transform 0.16s ease;
}
.cc-check:has(input:checked) .cc-box { background: #c98a3b; border-color: #c98a3b; }
.cc-check:has(input:checked) .cc-box::after { transform: scale(1); }

/* トグルスイッチ */
.cc-toggle {
  display: flex; align-items: center; justify-content: space-between;
  margin: 6px 0 4px; padding: 8px 2px;
  font-size: 0.85rem; cursor: pointer;
}
.cc-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.cc-switch {
  width: 42px; height: 23px; flex: none;
  background: #d6c3a4; border-radius: 999px; position: relative;
  transition: background 0.2s ease;
}
.cc-switch::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 17px; height: 17px; border-radius: 50%; background: #fff;
  transition: transform 0.2s ease;
}
.cc-toggle:has(input:checked) .cc-switch { background: #c98a3b; }
.cc-toggle:has(input:checked) .cc-switch::after { transform: translateX(19px); }

.mb-cart {
  width: 100%; margin-top: 12px; padding: 12px;
  font-size: 0.92rem; font-weight: 700; letter-spacing: 0.04em;
  color: #f5ede1;
  background: linear-gradient(135deg, #c98a3b, #a96e26);
  border: none; border-radius: 11px; cursor: pointer;
  box-shadow: 0 12px 22px -10px rgba(169, 110, 38, 0.8);
  transition: transform 0.15s ease;
}
.mb-cart:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .cc-opt, .cc-radio, .cc-radio::after, .cc-box, .cc-box::after,
  .cc-switch, .cc-switch::after, .mb-cart { transition: none; }
}
JavaScript
// 注文オプションに応じて合計金額を再計算(:has()装飾はCSSのみ)
const form = document.querySelector(".mb-order__form");
const cart = document.querySelector(".mb-cart");

if (form && cart) {
  const BASE = 500; // カフェラテ基本価格
  const milkPrice = { normal: 0, oat: 60, soy: 60 };

  // 現在の選択から合計を求める
  function total() {
    const milk = form.querySelector('input[name="milk"]:checked');
    let sum = BASE + (milk ? milkPrice[milk.value] : 0);
    if (form.shot && form.shot.checked) sum += 80;
    if (form.syrup && form.syrup.checked) sum += 50;
    return sum;
  }

  // ボタン表示を更新
  function refresh() {
    cart.textContent = `カートに入れる ¥${total().toLocaleString("ja-JP")}`;
  }

  form.addEventListener("change", refresh);

  form.addEventListener("submit", (e) => {
    e.preventDefault(); // 実送信しない
    const keep = cart.textContent;
    cart.textContent = "カートに追加しました ✓";
    cart.style.background = "linear-gradient(135deg,#3f7d4f,#5aa06a)";
    setTimeout(() => {
      cart.textContent = keep;
      cart.style.background = "";
    }, 1600);
  });

  refresh();
}

コード

HTML
<div class="stage">
  <div class="cc-card">
    <h2 class="cc-title">プランを選択</h2>

    <!-- カスタムラジオ:プラン選択 -->
    <div class="cc-group" role="radiogroup">
      <label class="cc-opt">
        <input type="radio" name="plan" value="free" checked>
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Free</strong><small>個人利用に</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="plan" value="pro">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Pro</strong><small>月額 ¥980</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="plan" value="team">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Team</strong><small>チーム向け</small></span>
      </label>
    </div>

    <h3 class="cc-sub">オプション</h3>

    <!-- カスタムチェック&トグル -->
    <label class="cc-check">
      <input type="checkbox" name="news" checked>
      <span class="cc-box"></span>
      <span>お知らせメールを受け取る</span>
    </label>
    <label class="cc-check">
      <input type="checkbox" name="terms">
      <span class="cc-box"></span>
      <span>利用規約に同意する</span>
    </label>

    <label class="cc-toggle">
      <span>ダークモード</span>
      <input type="checkbox" name="dark">
      <span class="cc-switch"></span>
    </label>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 50%, #fae8ff 100%);
  color: #1f2937;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.cc-card {
  width: min(380px, 92vw);
  padding: 26px 24px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 56px -26px rgba(79, 70, 229, 0.4);
}

.cc-title { margin: 0 0 16px; font-size: 1.1rem; color: #1e293b; }
.cc-sub { margin: 22px 0 12px; font-size: 0.82rem; color: #64748b; letter-spacing: 0.04em; }

/* ラジオを横並びカードに */
.cc-group { display: flex; gap: 10px; }
.cc-opt {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 14px 8px;
  border: 2px solid #e5e7eb;
  border-radius: 12px;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}
.cc-opt:hover { transform: translateY(-2px); }
.cc-opt input { position: absolute; opacity: 0; pointer-events: none; }

.cc-radio {
  width: 18px; height: 18px;
  border: 2px solid #cbd5e1;
  border-radius: 50%;
  position: relative;
  transition: border-color 0.18s ease;
}
.cc-radio::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: #6366f1;
  transform: scale(0);
  transition: transform 0.18s ease;
}
.cc-body { text-align: center; line-height: 1.3; }
.cc-body strong { display: block; font-size: 0.9rem; color: #1e293b; }
.cc-body small { font-size: 0.7rem; color: #94a3b8; }

/* チェック中のラジオカード */
.cc-opt:has(input:checked) { border-color: #6366f1; background: #eef2ff; }
.cc-opt:has(input:checked) .cc-radio { border-color: #6366f1; }
.cc-opt:has(input:checked) .cc-radio::after { transform: scale(1); }
.cc-opt input:focus-visible ~ .cc-radio { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

/* カスタムチェックボックス */
.cc-check {
  display: flex; align-items: center; gap: 11px;
  margin-bottom: 12px;
  font-size: 0.88rem;
  cursor: pointer;
}
.cc-check input { position: absolute; opacity: 0; pointer-events: none; }
.cc-box {
  width: 22px; height: 22px; flex: none;
  border: 2px solid #cbd5e1;
  border-radius: 7px;
  position: relative;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.cc-box::after {
  content: "";
  position: absolute;
  left: 6.5px; top: 2.5px;
  width: 5px; height: 10px;
  border: solid #fff;
  border-width: 0 2.5px 2.5px 0;
  transform: rotate(45deg) scale(0);
  transition: transform 0.18s ease;
}
.cc-check:has(input:checked) .cc-box { background: #6366f1; border-color: #6366f1; }
.cc-check:has(input:checked) .cc-box::after { transform: rotate(45deg) scale(1); }
.cc-check input:focus-visible ~ .cc-box { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

/* トグルスイッチ */
.cc-toggle {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 16px;
  font-size: 0.88rem;
  cursor: pointer;
}
.cc-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.cc-switch {
  width: 46px; height: 26px;
  background: #cbd5e1;
  border-radius: 999px;
  position: relative;
  transition: background 0.22s ease;
}
.cc-switch::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 20px; height: 20px;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 2px 5px rgba(0,0,0,0.25);
  transition: transform 0.22s ease;
}
.cc-toggle:has(input:checked) .cc-switch { background: #6366f1; }
.cc-toggle:has(input:checked) .cc-switch::after { transform: translateX(20px); }
.cc-toggle input:focus-visible ~ .cc-switch { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

@media (prefers-reduced-motion: reduce) {
  .cc-opt, .cc-radio, .cc-radio::after, .cc-box, .cc-box::after, .cc-switch, .cc-switch::after { transition: none; }
}
JavaScript
// ダークモードのトグルでカード配色を切り替えるデモ
const card = document.querySelector(".cc-card");
const dark = document.querySelector('input[name="dark"]');

if (card && dark) {
  dark.addEventListener("change", () => {
    if (dark.checked) {
      card.style.background = "#1e293b";
      card.style.color = "#e2e8f0";
    } else {
      card.style.background = "";
      card.style.color = "";
    }
  });
}

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

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

# 追加してほしい効果
カスタムチェック / ラジオ(フォーム & 入力)
:has() を活用してチェックボックス・カードラジオ・トグルスイッチをCSSだけで装飾。アクセシブルな見た目のリッチな選択UIを構築できます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
  <div class="cc-card">
    <h2 class="cc-title">プランを選択</h2>

    <!-- カスタムラジオ:プラン選択 -->
    <div class="cc-group" role="radiogroup">
      <label class="cc-opt">
        <input type="radio" name="plan" value="free" checked>
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Free</strong><small>個人利用に</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="plan" value="pro">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Pro</strong><small>月額 ¥980</small></span>
      </label>
      <label class="cc-opt">
        <input type="radio" name="plan" value="team">
        <span class="cc-radio"></span>
        <span class="cc-body"><strong>Team</strong><small>チーム向け</small></span>
      </label>
    </div>

    <h3 class="cc-sub">オプション</h3>

    <!-- カスタムチェック&トグル -->
    <label class="cc-check">
      <input type="checkbox" name="news" checked>
      <span class="cc-box"></span>
      <span>お知らせメールを受け取る</span>
    </label>
    <label class="cc-check">
      <input type="checkbox" name="terms">
      <span class="cc-box"></span>
      <span>利用規約に同意する</span>
    </label>

    <label class="cc-toggle">
      <span>ダークモード</span>
      <input type="checkbox" name="dark">
      <span class="cc-switch"></span>
    </label>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: linear-gradient(135deg, #eef2ff 0%, #e0e7ff 50%, #fae8ff 100%);
  color: #1f2937;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.cc-card {
  width: min(380px, 92vw);
  padding: 26px 24px;
  background: #fff;
  border-radius: 18px;
  box-shadow: 0 24px 56px -26px rgba(79, 70, 229, 0.4);
}

.cc-title { margin: 0 0 16px; font-size: 1.1rem; color: #1e293b; }
.cc-sub { margin: 22px 0 12px; font-size: 0.82rem; color: #64748b; letter-spacing: 0.04em; }

/* ラジオを横並びカードに */
.cc-group { display: flex; gap: 10px; }
.cc-opt {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 14px 8px;
  border: 2px solid #e5e7eb;
  border-radius: 12px;
  cursor: pointer;
  transition: border-color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}
.cc-opt:hover { transform: translateY(-2px); }
.cc-opt input { position: absolute; opacity: 0; pointer-events: none; }

.cc-radio {
  width: 18px; height: 18px;
  border: 2px solid #cbd5e1;
  border-radius: 50%;
  position: relative;
  transition: border-color 0.18s ease;
}
.cc-radio::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  background: #6366f1;
  transform: scale(0);
  transition: transform 0.18s ease;
}
.cc-body { text-align: center; line-height: 1.3; }
.cc-body strong { display: block; font-size: 0.9rem; color: #1e293b; }
.cc-body small { font-size: 0.7rem; color: #94a3b8; }

/* チェック中のラジオカード */
.cc-opt:has(input:checked) { border-color: #6366f1; background: #eef2ff; }
.cc-opt:has(input:checked) .cc-radio { border-color: #6366f1; }
.cc-opt:has(input:checked) .cc-radio::after { transform: scale(1); }
.cc-opt input:focus-visible ~ .cc-radio { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

/* カスタムチェックボックス */
.cc-check {
  display: flex; align-items: center; gap: 11px;
  margin-bottom: 12px;
  font-size: 0.88rem;
  cursor: pointer;
}
.cc-check input { position: absolute; opacity: 0; pointer-events: none; }
.cc-box {
  width: 22px; height: 22px; flex: none;
  border: 2px solid #cbd5e1;
  border-radius: 7px;
  position: relative;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.cc-box::after {
  content: "";
  position: absolute;
  left: 6.5px; top: 2.5px;
  width: 5px; height: 10px;
  border: solid #fff;
  border-width: 0 2.5px 2.5px 0;
  transform: rotate(45deg) scale(0);
  transition: transform 0.18s ease;
}
.cc-check:has(input:checked) .cc-box { background: #6366f1; border-color: #6366f1; }
.cc-check:has(input:checked) .cc-box::after { transform: rotate(45deg) scale(1); }
.cc-check input:focus-visible ~ .cc-box { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

/* トグルスイッチ */
.cc-toggle {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 16px;
  font-size: 0.88rem;
  cursor: pointer;
}
.cc-toggle input { position: absolute; opacity: 0; pointer-events: none; }
.cc-switch {
  width: 46px; height: 26px;
  background: #cbd5e1;
  border-radius: 999px;
  position: relative;
  transition: background 0.22s ease;
}
.cc-switch::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 20px; height: 20px;
  background: #fff;
  border-radius: 50%;
  box-shadow: 0 2px 5px rgba(0,0,0,0.25);
  transition: transform 0.22s ease;
}
.cc-toggle:has(input:checked) .cc-switch { background: #6366f1; }
.cc-toggle:has(input:checked) .cc-switch::after { transform: translateX(20px); }
.cc-toggle input:focus-visible ~ .cc-switch { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.22); }

@media (prefers-reduced-motion: reduce) {
  .cc-opt, .cc-radio, .cc-radio::after, .cc-box, .cc-box::after, .cc-switch, .cc-switch::after { transition: none; }
}

【JavaScript】
// ダークモードのトグルでカード配色を切り替えるデモ
const card = document.querySelector(".cc-card");
const dark = document.querySelector('input[name="dark"]');

if (card && dark) {
  dark.addEventListener("change", () => {
    if (dark.checked) {
      card.style.background = "#1e293b";
      card.style.color = "#e2e8f0";
    } else {
      card.style.background = "";
      card.style.color = "";
    }
  });
}

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

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