料金表 中級

月額/年額トグル切替の料金表

月額と年額をスイッチで切り替え、価格が切り替わる料金表。年額に「2ヶ月分お得」などの割引バッジを添えて年間契約へ誘導します。価格の動きで選びやすさと割安感を演出します。

#pricing#toggle#billing#discount

ライブデモ

使用例(お題: SaaS FlowDesk)

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

HTML
<!-- FlowDesk:月額/年額トグルの料金表 -->
<section class="ptg">
  <div class="ptg-switch">
    <span class="ptg-lab" id="ptgLabM">月額</span>
    <button class="ptg-toggle" id="ptgToggle" type="button" role="switch" aria-checked="true" aria-label="月額/年額切替"><span class="ptg-knob"></span></button>
    <span class="ptg-lab is-on" id="ptgLabY">年額 <em class="ptg-save">2ヶ月分お得</em></span>
  </div>

  <div class="ptg-grid" id="ptgGrid">
    <article class="ptg-card">
      <p class="ptg-name">Starter</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="980" data-y="9800">¥9,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
    <article class="ptg-card ptg-card--pop">
      <span class="ptg-badge">人気</span>
      <p class="ptg-name">Pro</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="1480" data-y="14800">¥14,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--solid" type="button">選ぶ</button>
    </article>
    <article class="ptg-card">
      <p class="ptg-name">Business</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="3800" data-y="38000">¥38,000</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
  </div>
</section>
CSS
/* FlowDesk(SaaS):月額/年額トグル料金表の再スキン */
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.ptg { width: 100%; min-height: 380px; display: grid; place-content: center; gap: 22px; padding: 26px; background: #0a0f22; color: #e7eaf7; }
.ptg-switch { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 13.5px; }
.ptg-lab { color: #8a93b8; font-weight: 600; transition: color .2s ease; }
.ptg-lab.is-on { color: #fff; }
.ptg-save { font-style: normal; font-size: 11px; font-weight: 700; color: #34d399; background: rgba(52,211,153,.14); padding: 2px 8px; border-radius: 999px; margin-left: 4px; }
.ptg-toggle { width: 46px; height: 26px; border-radius: 999px; border: none; cursor: pointer; background: #232a48; position: relative; transition: background .25s ease; }
.ptg-toggle[aria-checked="true"] { background: linear-gradient(135deg, #4f6bff, #6d28d9); }
.ptg-knob { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform .25s cubic-bezier(.2,.8,.2,1); }
.ptg-toggle[aria-checked="true"] .ptg-knob { transform: translateX(20px); }
.ptg-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: min(760px, 94vw); }
.ptg-card { position: relative; background: #131a30; border: 1px solid #222a48; border-radius: 16px; padding: 22px 18px; text-align: center; }
.ptg-card--pop { border-color: #4f6bff; box-shadow: 0 18px 44px rgba(79,107,255,.25); }
.ptg-badge { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); font-size: 10.5px; font-weight: 800; color: #fff; background: linear-gradient(135deg, #4f6bff, #6d28d9); padding: 4px 12px; border-radius: 999px; }
.ptg-name { margin: 0 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #8a93b8; }
.ptg-price { margin: 0 0 16px; }
.ptg-amt { font-size: 30px; font-weight: 900; letter-spacing: -.02em; color: #fff; font-variant-numeric: tabular-nums; }
.ptg-per { font-size: 12px; color: #8a93b8; margin-left: 2px; }
.ptg-btn { width: 100%; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; padding: 10px 0; border-radius: 10px; transition: filter .15s ease; }
.ptg-btn--solid { border: none; color: #fff; background: linear-gradient(135deg, #4f6bff, #6d28d9); }
.ptg-btn--solid:hover { filter: brightness(1.08); }
.ptg-btn--ghost { background: #1b2240; border: 1px solid #2a3252; color: #c4caea; }
.ptg-btn--ghost:hover { background: #222a4c; }

@media (max-width: 620px) { .ptg-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .ptg-toggle, .ptg-knob { transition: none; } }
JavaScript
// (デモと同じフックを流用)月額/年額トグルで価格を切替+自動実演
(() => {
  const toggle = document.getElementById('ptgToggle');
  const grid = document.getElementById('ptgGrid');
  const labM = document.getElementById('ptgLabM');
  const labY = document.getElementById('ptgLabY');
  if (!toggle || !grid) return;
  const amts = [...grid.querySelectorAll('.ptg-amt')];
  const pers = [...grid.querySelectorAll('.ptg-per')];
  const yen = (n) => '¥' + Number(n).toLocaleString('en-US');
  function apply(yearly) {
    toggle.setAttribute('aria-checked', yearly ? 'true' : 'false');
    amts.forEach(a => a.textContent = yen(yearly ? a.dataset.y : a.dataset.m));
    pers.forEach(p => p.textContent = yearly ? '/年' : '/月');
    if (labM) labM.classList.toggle('is-on', !yearly);
    if (labY) labY.classList.toggle('is-on', yearly);
  }
  let yearly = true; apply(yearly);
  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  toggle.addEventListener('click', () => { auto = false; yearly = !yearly; apply(yearly); });
  if (auto) { const tick = () => { if (!auto) return; yearly = !yearly; apply(yearly); setTimeout(tick, 2200); }; setTimeout(tick, 2000); }
})();

実装ガイド

使いどころ

年間契約へ誘導したい料金表に。月額/年額をスイッチで切り替えると価格が変わり、年額に割引バッジを添えて割安感を示します。

実装時の注意点

data-m / data-y に月額・年額を持たせ、トグルで表示価格と単位(/月・/年)を差し替えます。価格は桁区切り表示。プレビューでは自動でも切り替えます。

対応ブラウザ

transform・transition・Intl 桁区切りは全モダンブラウザ対応。

よくある失敗

年額表示を『年額総額』にするか『月額換算』にするかを明確に(誤認防止)。割引率の根拠(2ヶ月分等)を添えると親切。トグルの状態は aria-checked で支援技術にも伝えます。

応用例

通貨切替、3段階以上の課金周期、プランごとの割引率表示、選択中プランの強調などに展開できます。

コード

HTML
<!-- 月額/年額トグル切替の料金表 -->
<section class="ptg">
  <div class="ptg-switch">
    <span class="ptg-lab" id="ptgLabM">月額</span>
    <button class="ptg-toggle" id="ptgToggle" type="button" role="switch" aria-checked="true" aria-label="月額/年額切替"><span class="ptg-knob"></span></button>
    <span class="ptg-lab is-on" id="ptgLabY">年額 <em class="ptg-save">2ヶ月分お得</em></span>
  </div>

  <div class="ptg-grid" id="ptgGrid">
    <article class="ptg-card">
      <p class="ptg-name">Starter</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="980" data-y="9800">¥9,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
    <article class="ptg-card ptg-card--pop">
      <span class="ptg-badge">人気</span>
      <p class="ptg-name">Pro</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="1480" data-y="14800">¥14,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--solid" type="button">選ぶ</button>
    </article>
    <article class="ptg-card">
      <p class="ptg-name">Team</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="3800" data-y="38000">¥38,000</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
  </div>
</section>
CSS
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.ptg { width: 100%; min-height: 380px; display: grid; place-content: center; gap: 22px; padding: 26px; background: #0e1222; color: #e7eaf7; }

.ptg-switch { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 13.5px; }
.ptg-lab { color: #8c93b8; font-weight: 600; transition: color .2s ease; }
.ptg-lab.is-on { color: #fff; }
.ptg-save { font-style: normal; font-size: 11px; font-weight: 700; color: #34d399; background: rgba(52,211,153,.14); padding: 2px 8px; border-radius: 999px; margin-left: 4px; }
.ptg-toggle { width: 46px; height: 26px; border-radius: 999px; border: none; cursor: pointer; background: #2a3152; position: relative; transition: background .25s ease; }
.ptg-toggle[aria-checked="true"] { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.ptg-knob { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform .25s cubic-bezier(.2,.8,.2,1); }
.ptg-toggle[aria-checked="true"] .ptg-knob { transform: translateX(20px); }

.ptg-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: min(760px, 94vw); }
.ptg-card { position: relative; background: #161b2e; border: 1px solid #242a44; border-radius: 16px; padding: 22px 18px; text-align: center; }
.ptg-card--pop { border-color: #6366f1; box-shadow: 0 18px 44px rgba(99,102,241,.25); }
.ptg-badge { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); font-size: 10.5px; font-weight: 800; color: #fff; background: linear-gradient(135deg, #6366f1, #db2777); padding: 4px 12px; border-radius: 999px; }
.ptg-name { margin: 0 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #8c93b8; }
.ptg-price { margin: 0 0 16px; }
.ptg-amt { font-size: 30px; font-weight: 900; letter-spacing: -.02em; color: #fff; font-variant-numeric: tabular-nums; }
.ptg-per { font-size: 12px; color: #8c93b8; margin-left: 2px; }
.ptg-btn { width: 100%; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; padding: 10px 0; border-radius: 10px; transition: filter .15s ease; }
.ptg-btn--solid { border: none; color: #fff; background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.ptg-btn--solid:hover { filter: brightness(1.08); }
.ptg-btn--ghost { background: #1d2440; border: 1px solid #2a3152; color: #c4caea; }
.ptg-btn--ghost:hover { background: #232b4c; }

@media (max-width: 620px) { .ptg-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .ptg-toggle, .ptg-knob { transition: none; } }
JavaScript
// 月額/年額トグルで価格を切替。プレビューでは自動でも切替を実演
(() => {
  const toggle = document.getElementById('ptgToggle');
  const grid = document.getElementById('ptgGrid');
  const labM = document.getElementById('ptgLabM');
  const labY = document.getElementById('ptgLabY');
  if (!toggle || !grid) return;
  const amts = [...grid.querySelectorAll('.ptg-amt')];
  const pers = [...grid.querySelectorAll('.ptg-per')];
  const yen = (n) => '¥' + Number(n).toLocaleString('en-US');

  function apply(yearly) {
    toggle.setAttribute('aria-checked', yearly ? 'true' : 'false');
    amts.forEach(a => a.textContent = yen(yearly ? a.dataset.y : a.dataset.m));
    pers.forEach(p => p.textContent = yearly ? '/年' : '/月');
    if (labM) labM.classList.toggle('is-on', !yearly);
    if (labY) labY.classList.toggle('is-on', yearly);
  }
  let yearly = true;
  apply(yearly);

  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  toggle.addEventListener('click', () => { auto = false; yearly = !yearly; apply(yearly); });
  if (auto) {
    const tick = () => { if (!auto) return; yearly = !yearly; apply(yearly); setTimeout(tick, 2200); };
    setTimeout(tick, 2000);
  }
})();

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

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

# 追加してほしい効果
月額/年額トグル切替の料金表(料金表)
月額と年額をスイッチで切り替え、価格が切り替わる料金表。年額に「2ヶ月分お得」などの割引バッジを添えて年間契約へ誘導します。価格の動きで選びやすさと割安感を演出します。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 月額/年額トグル切替の料金表 -->
<section class="ptg">
  <div class="ptg-switch">
    <span class="ptg-lab" id="ptgLabM">月額</span>
    <button class="ptg-toggle" id="ptgToggle" type="button" role="switch" aria-checked="true" aria-label="月額/年額切替"><span class="ptg-knob"></span></button>
    <span class="ptg-lab is-on" id="ptgLabY">年額 <em class="ptg-save">2ヶ月分お得</em></span>
  </div>

  <div class="ptg-grid" id="ptgGrid">
    <article class="ptg-card">
      <p class="ptg-name">Starter</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="980" data-y="9800">¥9,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
    <article class="ptg-card ptg-card--pop">
      <span class="ptg-badge">人気</span>
      <p class="ptg-name">Pro</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="1480" data-y="14800">¥14,800</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--solid" type="button">選ぶ</button>
    </article>
    <article class="ptg-card">
      <p class="ptg-name">Team</p>
      <p class="ptg-price"><b class="ptg-amt" data-m="3800" data-y="38000">¥38,000</b><span class="ptg-per">/年</span></p>
      <button class="ptg-btn ptg-btn--ghost" type="button">選ぶ</button>
    </article>
  </div>
</section>

【CSS】
* { box-sizing: border-box; }
body { margin: 0; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; }

.ptg { width: 100%; min-height: 380px; display: grid; place-content: center; gap: 22px; padding: 26px; background: #0e1222; color: #e7eaf7; }

.ptg-switch { display: flex; align-items: center; justify-content: center; gap: 12px; font-size: 13.5px; }
.ptg-lab { color: #8c93b8; font-weight: 600; transition: color .2s ease; }
.ptg-lab.is-on { color: #fff; }
.ptg-save { font-style: normal; font-size: 11px; font-weight: 700; color: #34d399; background: rgba(52,211,153,.14); padding: 2px 8px; border-radius: 999px; margin-left: 4px; }
.ptg-toggle { width: 46px; height: 26px; border-radius: 999px; border: none; cursor: pointer; background: #2a3152; position: relative; transition: background .25s ease; }
.ptg-toggle[aria-checked="true"] { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.ptg-knob { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform .25s cubic-bezier(.2,.8,.2,1); }
.ptg-toggle[aria-checked="true"] .ptg-knob { transform: translateX(20px); }

.ptg-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; width: min(760px, 94vw); }
.ptg-card { position: relative; background: #161b2e; border: 1px solid #242a44; border-radius: 16px; padding: 22px 18px; text-align: center; }
.ptg-card--pop { border-color: #6366f1; box-shadow: 0 18px 44px rgba(99,102,241,.25); }
.ptg-badge { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); font-size: 10.5px; font-weight: 800; color: #fff; background: linear-gradient(135deg, #6366f1, #db2777); padding: 4px 12px; border-radius: 999px; }
.ptg-name { margin: 0 0 10px; font-size: 12px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: #8c93b8; }
.ptg-price { margin: 0 0 16px; }
.ptg-amt { font-size: 30px; font-weight: 900; letter-spacing: -.02em; color: #fff; font-variant-numeric: tabular-nums; }
.ptg-per { font-size: 12px; color: #8c93b8; margin-left: 2px; }
.ptg-btn { width: 100%; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; padding: 10px 0; border-radius: 10px; transition: filter .15s ease; }
.ptg-btn--solid { border: none; color: #fff; background: linear-gradient(135deg, #6366f1, #8b5cf6); }
.ptg-btn--solid:hover { filter: brightness(1.08); }
.ptg-btn--ghost { background: #1d2440; border: 1px solid #2a3152; color: #c4caea; }
.ptg-btn--ghost:hover { background: #232b4c; }

@media (max-width: 620px) { .ptg-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .ptg-toggle, .ptg-knob { transition: none; } }

【JavaScript】
// 月額/年額トグルで価格を切替。プレビューでは自動でも切替を実演
(() => {
  const toggle = document.getElementById('ptgToggle');
  const grid = document.getElementById('ptgGrid');
  const labM = document.getElementById('ptgLabM');
  const labY = document.getElementById('ptgLabY');
  if (!toggle || !grid) return;
  const amts = [...grid.querySelectorAll('.ptg-amt')];
  const pers = [...grid.querySelectorAll('.ptg-per')];
  const yen = (n) => '¥' + Number(n).toLocaleString('en-US');

  function apply(yearly) {
    toggle.setAttribute('aria-checked', yearly ? 'true' : 'false');
    amts.forEach(a => a.textContent = yen(yearly ? a.dataset.y : a.dataset.m));
    pers.forEach(p => p.textContent = yearly ? '/年' : '/月');
    if (labM) labM.classList.toggle('is-on', !yearly);
    if (labY) labY.classList.toggle('is-on', yearly);
  }
  let yearly = true;
  apply(yearly);

  let auto = !matchMedia('(prefers-reduced-motion: reduce)').matches;
  toggle.addEventListener('click', () => { auto = false; yearly = !yearly; apply(yearly); });
  if (auto) {
    const tick = () => { if (!auto) return; yearly = !yearly; apply(yearly); setTimeout(tick, 2200); };
    setTimeout(tick, 2000);
  }
})();

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

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