数量ステッパー
「−」「+」ボタンで数量を増減できる入力欄。直接入力にも対応し、上限・下限に達するとボタンが自動的に無効化され、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();
})();
🤖 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。