数量ステッパー

「−」「+」ボタンで数量を増減できる入力欄。直接入力にも対応し、上限・下限に達するとボタンが自動的に無効化され、ECサイトのカート等で使えます。

#css#js#form#stepper

ライブデモ

コード

HTML
<div class="stage">
  <div class="qs-card">
    <h2 class="qs-title">カート内の数量</h2>
    <p class="qs-item">オリジナルTシャツ(Mサイズ)</p>

    <div class="qs-stepper">
      <button type="button" class="qs-btn" id="qs-dec" aria-label="数量を1減らす">
        <span aria-hidden="true">−</span>
      </button>
      <label class="qs-field">
        <span class="qs-field-label">数量</span>
        <input type="number" id="qs-input" class="qs-input" value="1" min="1" max="10" step="1" inputmode="numeric" aria-describedby="qs-hint">
      </label>
      <button type="button" class="qs-btn" id="qs-inc" aria-label="数量を1増やす">
        <span aria-hidden="true">+</span>
      </button>
    </div>

    <p class="qs-hint" id="qs-hint" role="status">在庫は10点まで注文できます</p>
  </div>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  /* 上品な暖色グラデ背景 */
  background: linear-gradient(135deg, #fdf6ec 0%, #f8e8d4 55%, #f2d9bd 100%);
  color: #3a2f28;
}

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

.qs-card {
  width: min(340px, 92vw);
  padding: 30px 28px 26px;
  background: #fff;
  border-radius: 20px;
  box-shadow: 0 24px 56px -26px rgba(120, 72, 32, 0.35);
  text-align: center;
}

.qs-title {
  margin: 0 0 6px;
  font-size: 1.1rem;
  font-weight: 700;
  color: #4b3a2c;
}

.qs-item {
  margin: 0 0 22px;
  font-size: 0.85rem;
  color: #9a8a7a;
}

.qs-stepper {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 10px;
}

.qs-btn {
  flex: none;
  width: 46px;
  height: 46px;
  border: none;
  border-radius: 14px;
  background: #fbeee0;
  color: #b5651d;
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 0.16s ease, transform 0.12s ease, opacity 0.16s ease;
}
.qs-btn:hover:not(:disabled) { background: #f6dfc4; transform: translateY(-1px); }
.qs-btn:active:not(:disabled) { transform: translateY(0) scale(0.94); }
.qs-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.35); }
.qs-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }

.qs-field {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 84px;
}
.qs-field-label {
  font-size: 0.68rem;
  color: #b39c85;
  letter-spacing: 0.06em;
}

.qs-input {
  width: 100%;
  height: 46px;
  border: 2px solid #efe0cf;
  border-radius: 14px;
  background: #fffaf3;
  text-align: center;
  font-size: 1.1rem;
  font-weight: 700;
  color: #4b3a2c;
  padding: 0;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
  /* ネイティブのスピンボタンは非表示にして自作ボタンに統一 */
  -moz-appearance: textfield;
}
.qs-input::-webkit-outer-spin-button,
.qs-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qs-input:focus {
  outline: none;
  border-color: #d97706;
  box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.16);
}

/* 増減時の軽いバウンス演出 */
.qs-field.is-bump .qs-input { animation: qs-bump 0.22s ease; }
@keyframes qs-bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.08); }
  100% { transform: scale(1); }
}

.qs-hint {
  margin: 18px 0 0;
  font-size: 0.78rem;
  color: #9a8a7a;
  transition: color 0.2s ease;
}
.qs-hint.is-limit { color: #c2410c; font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .qs-btn, .qs-input, .qs-hint { transition: none; }
  .qs-field.is-bump .qs-input { animation: none; }
}
JavaScript
// 数量ステッパー:±ボタンと直接入力を同期させ、上限/下限でボタンを無効化する
(function () {
  var input = document.getElementById("qs-input");
  var decBtn = document.getElementById("qs-dec");
  var incBtn = document.getElementById("qs-inc");
  var hint = document.getElementById("qs-hint");
  var field = input ? input.closest(".qs-field") : null;
  if (!input || !decBtn || !incBtn || !hint) return;

  var min = parseInt(input.min, 10) || 1;
  var max = parseInt(input.max, 10) || 10;

  function clamp(n) {
    if (isNaN(n)) return min;
    return Math.min(max, Math.max(min, n));
  }

  function bump() {
    if (!field) return;
    field.classList.remove("is-bump");
    // reflow してから付け直すことでアニメーションを再トリガー
    void field.offsetWidth;
    field.classList.add("is-bump");
  }

  function render() {
    var value = clamp(parseInt(input.value, 10));
    input.value = String(value);
    decBtn.disabled = value <= min;
    incBtn.disabled = value >= max;

    if (value >= max) {
      hint.textContent = "上限(" + max + "点)に達しました";
      hint.classList.add("is-limit");
    } else if (value <= min) {
      hint.textContent = "最低" + min + "点からご注文いただけます";
      hint.classList.remove("is-limit");
    } else {
      hint.textContent = "在庫は" + max + "点まで注文できます";
      hint.classList.remove("is-limit");
    }
  }

  function step(delta) {
    var current = clamp(parseInt(input.value, 10));
    var next = clamp(current + delta);
    if (next === current) { render(); return; }
    input.value = String(next);
    bump();
    render();
  }

  decBtn.addEventListener("click", function () { step(-1); });
  incBtn.addEventListener("click", function () { step(1); });
  input.addEventListener("input", render);
  input.addEventListener("blur", render);

  render();
})();

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

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

# 追加してほしい効果
数量ステッパー(フォーム & 入力)
「−」「+」ボタンで数量を増減できる入力欄。直接入力にも対応し、上限・下限に達するとボタンが自動的に無効化され、ECサイトのカート等で使えます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
  <div class="qs-card">
    <h2 class="qs-title">カート内の数量</h2>
    <p class="qs-item">オリジナルTシャツ(Mサイズ)</p>

    <div class="qs-stepper">
      <button type="button" class="qs-btn" id="qs-dec" aria-label="数量を1減らす">
        <span aria-hidden="true">−</span>
      </button>
      <label class="qs-field">
        <span class="qs-field-label">数量</span>
        <input type="number" id="qs-input" class="qs-input" value="1" min="1" max="10" step="1" inputmode="numeric" aria-describedby="qs-hint">
      </label>
      <button type="button" class="qs-btn" id="qs-inc" aria-label="数量を1増やす">
        <span aria-hidden="true">+</span>
      </button>
    </div>

    <p class="qs-hint" id="qs-hint" role="status">在庫は10点まで注文できます</p>
  </div>
</div>

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

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  /* 上品な暖色グラデ背景 */
  background: linear-gradient(135deg, #fdf6ec 0%, #f8e8d4 55%, #f2d9bd 100%);
  color: #3a2f28;
}

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

.qs-card {
  width: min(340px, 92vw);
  padding: 30px 28px 26px;
  background: #fff;
  border-radius: 20px;
  box-shadow: 0 24px 56px -26px rgba(120, 72, 32, 0.35);
  text-align: center;
}

.qs-title {
  margin: 0 0 6px;
  font-size: 1.1rem;
  font-weight: 700;
  color: #4b3a2c;
}

.qs-item {
  margin: 0 0 22px;
  font-size: 0.85rem;
  color: #9a8a7a;
}

.qs-stepper {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 10px;
}

.qs-btn {
  flex: none;
  width: 46px;
  height: 46px;
  border: none;
  border-radius: 14px;
  background: #fbeee0;
  color: #b5651d;
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 0.16s ease, transform 0.12s ease, opacity 0.16s ease;
}
.qs-btn:hover:not(:disabled) { background: #f6dfc4; transform: translateY(-1px); }
.qs-btn:active:not(:disabled) { transform: translateY(0) scale(0.94); }
.qs-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.35); }
.qs-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }

.qs-field {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 84px;
}
.qs-field-label {
  font-size: 0.68rem;
  color: #b39c85;
  letter-spacing: 0.06em;
}

.qs-input {
  width: 100%;
  height: 46px;
  border: 2px solid #efe0cf;
  border-radius: 14px;
  background: #fffaf3;
  text-align: center;
  font-size: 1.1rem;
  font-weight: 700;
  color: #4b3a2c;
  padding: 0;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
  /* ネイティブのスピンボタンは非表示にして自作ボタンに統一 */
  -moz-appearance: textfield;
}
.qs-input::-webkit-outer-spin-button,
.qs-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.qs-input:focus {
  outline: none;
  border-color: #d97706;
  box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.16);
}

/* 増減時の軽いバウンス演出 */
.qs-field.is-bump .qs-input { animation: qs-bump 0.22s ease; }
@keyframes qs-bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.08); }
  100% { transform: scale(1); }
}

.qs-hint {
  margin: 18px 0 0;
  font-size: 0.78rem;
  color: #9a8a7a;
  transition: color 0.2s ease;
}
.qs-hint.is-limit { color: #c2410c; font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .qs-btn, .qs-input, .qs-hint { transition: none; }
  .qs-field.is-bump .qs-input { animation: none; }
}

【JavaScript】
// 数量ステッパー:±ボタンと直接入力を同期させ、上限/下限でボタンを無効化する
(function () {
  var input = document.getElementById("qs-input");
  var decBtn = document.getElementById("qs-dec");
  var incBtn = document.getElementById("qs-inc");
  var hint = document.getElementById("qs-hint");
  var field = input ? input.closest(".qs-field") : null;
  if (!input || !decBtn || !incBtn || !hint) return;

  var min = parseInt(input.min, 10) || 1;
  var max = parseInt(input.max, 10) || 10;

  function clamp(n) {
    if (isNaN(n)) return min;
    return Math.min(max, Math.max(min, n));
  }

  function bump() {
    if (!field) return;
    field.classList.remove("is-bump");
    // reflow してから付け直すことでアニメーションを再トリガー
    void field.offsetWidth;
    field.classList.add("is-bump");
  }

  function render() {
    var value = clamp(parseInt(input.value, 10));
    input.value = String(value);
    decBtn.disabled = value <= min;
    incBtn.disabled = value >= max;

    if (value >= max) {
      hint.textContent = "上限(" + max + "点)に達しました";
      hint.classList.add("is-limit");
    } else if (value <= min) {
      hint.textContent = "最低" + min + "点からご注文いただけます";
      hint.classList.remove("is-limit");
    } else {
      hint.textContent = "在庫は" + max + "点まで注文できます";
      hint.classList.remove("is-limit");
    }
  }

  function step(delta) {
    var current = clamp(parseInt(input.value, 10));
    var next = clamp(current + delta);
    if (next === current) { render(); return; }
    input.value = String(next);
    bump();
    render();
  }

  decBtn.addEventListener("click", function () { step(-1); });
  incBtn.addEventListener("click", function () { step(1); });
  input.addEventListener("input", render);
  input.addEventListener("blur", render);

  render();
})();

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

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