レンジスライダー(単一・価格帯)
進捗グラデ付きの単一スライダーと、2ハンドルの価格帯スライダー。input[type=range]の装飾で、フィルターや絞り込みに使えます。
ライブデモ
使用例(お題: SaaS FlowDesk)
この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。
HTML
<!-- FlowDesk:料金シミュレーター(シート数スライダー+予算帯フィルター) -->
<div class="app">
<header class="app__bar">
<span class="app__logo">💼 FlowDesk</span>
<span class="app__tag">料金シミュレーター</span>
</header>
<main class="calc">
<!-- 単一スライダー:利用シート数 -->
<section class="field">
<div class="field__top">
<label for="seats">利用シート数</label>
<span class="field__val" id="seatsVal">10席</span>
</div>
<input type="range" id="seats" class="rng rng--single" min="1" max="50" value="10">
<p class="calc__total">月額 <strong id="total">¥14,800</strong> <span>(¥1,480 / 席)</span></p>
</section>
<!-- 2ハンドル:予算帯で絞り込み -->
<section class="field" data-dual>
<div class="field__top">
<label>月額予算で絞り込み</label>
<span class="field__val" id="budgetVal">¥10,000 – ¥40,000</span>
</div>
<div class="dual">
<span class="dual__track"></span>
<span class="dual__fill" data-fill></span>
<input type="range" data-lo min="0" max="60000" step="1000" value="10000">
<input type="range" data-hi min="0" max="60000" step="1000" value="40000">
</div>
</section>
</main>
</div>
CSS
/* FlowDesk SaaS テーマ */
:root{--navy:#0f1b34;--blue:#4f7cff;--ink:#1d2740;--line:#e3e8f2;--muted:#6b7794;--bg:#f4f6fb}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--ink)}
.app{max-width:480px;margin:0 auto;min-height:100vh;display:flex;flex-direction:column}
.app__bar{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;background:var(--navy);color:#fff}
.app__logo{font-weight:700}
.app__tag{font-size:.78rem;color:#aab6d6}
.calc{flex:1;padding:24px 22px;display:flex;flex-direction:column;gap:26px}
.field__top{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px;font-size:.9rem;font-weight:600}
.field__val{color:var(--blue);font-weight:800}
.calc__total{margin:14px 0 0;font-size:.9rem;color:var(--muted)}
.calc__total strong{font-size:1.4rem;color:var(--navy);margin:0 4px}
.calc__total span{font-size:.76rem}
/* 単一スライダー:進捗グラデ */
.rng--single{
-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:6px;
background:linear-gradient(var(--blue),var(--blue)) 0/calc((var(--p,10) - 1)/49*100%) 100% no-repeat,var(--line);
}
.rng--single::-webkit-slider-thumb{-webkit-appearance:none;width:20px;height:20px;border-radius:50%;background:#fff;border:3px solid var(--blue);cursor:pointer;box-shadow:0 2px 8px rgba(79,124,255,.4)}
.rng--single::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:#fff;border:3px solid var(--blue);cursor:pointer}
/* 2ハンドル価格帯 */
.dual{position:relative;height:24px}
.dual__track{position:absolute;top:50%;left:0;right:0;height:6px;transform:translateY(-50%);background:var(--line);border-radius:6px}
.dual__fill{position:absolute;top:50%;height:6px;transform:translateY(-50%);background:var(--blue);border-radius:6px}
.dual input{
-webkit-appearance:none;appearance:none;position:absolute;top:0;left:0;width:100%;height:24px;
background:none;pointer-events:none;margin:0;
}
.dual input::-webkit-slider-thumb{-webkit-appearance:none;pointer-events:auto;width:20px;height:20px;border-radius:50%;background:#fff;border:3px solid var(--blue);cursor:pointer;box-shadow:0 2px 8px rgba(79,124,255,.4)}
.dual input::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;border-radius:50%;background:#fff;border:3px solid var(--blue);cursor:pointer}
JavaScript
// 円表示ヘルパー
const yen = (n) => '¥' + Number(n).toLocaleString('ja-JP');
// 単一スライダー:シート数 → 月額を再計算
const seats = document.getElementById('seats');
const seatsVal = document.getElementById('seatsVal');
const total = document.getElementById('total');
const PRICE = 1480;
const updateSeats = () => {
if (!seats) return;
const n = +seats.value;
seats.style.setProperty('--p', n); // 進捗グラデ用
if (seatsVal) seatsVal.textContent = n + '席';
if (total) total.textContent = yen(n * PRICE);
};
seats?.addEventListener('input', updateSeats);
updateSeats();
// 2ハンドル予算帯スライダー
const dual = document.querySelector('[data-dual]');
if (dual) {
const lo = dual.querySelector('[data-lo]');
const hi = dual.querySelector('[data-hi]');
const fill = dual.querySelector('[data-fill]');
const out = document.getElementById('budgetVal');
const MIN = +lo.min, MAX = +lo.max;
const updateDual = () => {
let loV = +lo.value, hiV = +hi.value;
// ハンドルの追い越しを防ぐ
if (loV > hiV) { [loV, hiV] = [hiV, loV]; lo.value = loV; hi.value = hiV; }
const span = MAX - MIN || 1;
const lp = ((loV - MIN) / span) * 100;
const hp = ((hiV - MIN) / span) * 100;
fill.style.left = lp + '%';
fill.style.width = (hp - lp) + '%';
if (out) out.textContent = `${yen(loV)} – ${yen(hiV)}`;
};
lo.addEventListener('input', updateDual);
hi.addEventListener('input', updateDual);
updateDual();
}
コード
HTML
<!-- レンジスライダー:単一値と2ハンドル(価格帯)。値はバブルで表示 -->
<div class="rng">
<h2 class="rng__title">フィルター設定</h2>
<!-- 単一値スライダー -->
<div class="rng__group">
<div class="rng__label"><span>明るさ</span><span class="rng__val" id="brightVal">60%</span></div>
<input class="slider" id="bright" type="range" min="0" max="100" value="60" aria-label="明るさ">
</div>
<!-- 2ハンドル(価格帯) -->
<div class="rng__group">
<div class="rng__label"><span>価格帯</span><span class="rng__val" id="priceVal">¥2,000 – ¥7,000</span></div>
<div class="dual" data-dual>
<div class="dual__track"><div class="dual__fill" data-fill></div></div>
<input class="slider slider--ghost" type="range" min="0" max="10000" step="500" value="2000" data-lo aria-label="下限価格">
<input class="slider slider--ghost" type="range" min="0" max="10000" step="500" value="7000" data-hi aria-label="上限価格">
</div>
</div>
</div>
CSS
:root{
--bg:#0f1729;
--card:#172036;
--accent:#22d3ee;
--accent2:#a78bfa;
--text:#e6edf7;
--muted:#8b97b5;
--track:#2a3553;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;
display:grid;place-items:center;padding:26px 16px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(700px 360px at 50% -10%,#1b2c4a,transparent),var(--bg);
}
.rng{
width:min(420px,100%);
background:var(--card);border:1px solid #25304e;border-radius:18px;
padding:26px 24px;
}
.rng__title{margin:0 0 22px;font-size:1.15rem;display:flex;align-items:center;gap:10px}
.rng__title::before{
content:"";width:8px;height:20px;border-radius:4px;
background:linear-gradient(var(--accent),var(--accent2));
}
.rng__group{margin-bottom:26px}
.rng__group:last-child{margin-bottom:0}
.rng__label{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
.rng__label span:first-child{color:var(--muted);font-size:.9rem}
.rng__val{font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums}
/* 共通スライダー外観(クロスブラウザ) */
.slider{
-webkit-appearance:none;appearance:none;width:100%;height:6px;
background:var(--track);border-radius:999px;outline:none;margin:0;
/* 進捗をグラデで表現(--p は0〜100) */
background-image:linear-gradient(90deg,var(--accent),var(--accent2));
background-repeat:no-repeat;
background-size:calc(var(--p,60) * 1%) 100%;
}
.slider::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:20px;height:20px;border-radius:50%;cursor:pointer;
background:#fff;border:3px solid var(--accent);
box-shadow:0 4px 10px rgba(0,0,0,.4);transition:transform .15s;
}
.slider::-webkit-slider-thumb:active{transform:scale(1.18)}
.slider::-moz-range-thumb{
width:18px;height:18px;border-radius:50%;cursor:pointer;
background:#fff;border:3px solid var(--accent);
}
.slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--accent2);outline-offset:2px}
/* 2ハンドル:透明スライダーを重ねる方式 */
.dual{position:relative;height:24px;display:flex;align-items:center}
.dual__track{
position:absolute;left:0;right:0;height:6px;border-radius:999px;background:var(--track);
}
.dual__fill{
position:absolute;height:6px;border-radius:999px;
background:linear-gradient(90deg,var(--accent),var(--accent2));
}
.slider--ghost{
position:absolute;left:0;right:0;width:100%;background:none;background-image:none;
pointer-events:none;height:24px;
}
.slider--ghost::-webkit-slider-thumb{pointer-events:auto}
.slider--ghost::-moz-range-thumb{pointer-events:auto}
@media (prefers-reduced-motion:reduce){.slider::-webkit-slider-thumb{transition:none}}
JavaScript
// 単一スライダー:進捗グラデと値表示を更新
const bright = document.getElementById('bright');
const brightVal = document.getElementById('brightVal');
const updateSingle = () => {
if (!bright || !brightVal) return;
bright.style.setProperty('--p', bright.value); // 0-100をそのまま%に
brightVal.textContent = bright.value + '%';
};
bright?.addEventListener('input', updateSingle);
updateSingle();
// 2ハンドル価格帯スライダー
const dual = document.querySelector('[data-dual]');
if (dual) {
const lo = dual.querySelector('[data-lo]');
const hi = dual.querySelector('[data-hi]');
const fill = dual.querySelector('[data-fill]');
const out = document.getElementById('priceVal');
const MIN = +lo.min, MAX = +lo.max;
const yen = (n) => '¥' + Number(n).toLocaleString('ja-JP');
const updateDual = () => {
let loV = +lo.value, hiV = +hi.value;
// ハンドルの追い越しを防ぐ
if (loV > hiV) { [loV, hiV] = [hiV, loV]; lo.value = loV; hi.value = hiV; }
const span = MAX - MIN || 1;
const lp = ((loV - MIN) / span) * 100;
const hp = ((hiV - MIN) / span) * 100;
fill.style.left = lp + '%';
fill.style.width = (hp - lp) + '%';
if (out) out.textContent = `${yen(loV)} – ${yen(hiV)}`;
};
lo.addEventListener('input', updateDual);
hi.addEventListener('input', updateDual);
updateDual();
}
🤖 AIエージェント用プロンプト
このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「レンジスライダー(単一・価格帯)」の効果を追加してください。
# 追加してほしい効果
レンジスライダー(単一・価格帯)(UIコンポーネント)
進捗グラデ付きの単一スライダーと、2ハンドルの価格帯スライダー。input[type=range]の装飾で、フィルターや絞り込みに使えます。
# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】
# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- レンジスライダー:単一値と2ハンドル(価格帯)。値はバブルで表示 -->
<div class="rng">
<h2 class="rng__title">フィルター設定</h2>
<!-- 単一値スライダー -->
<div class="rng__group">
<div class="rng__label"><span>明るさ</span><span class="rng__val" id="brightVal">60%</span></div>
<input class="slider" id="bright" type="range" min="0" max="100" value="60" aria-label="明るさ">
</div>
<!-- 2ハンドル(価格帯) -->
<div class="rng__group">
<div class="rng__label"><span>価格帯</span><span class="rng__val" id="priceVal">¥2,000 – ¥7,000</span></div>
<div class="dual" data-dual>
<div class="dual__track"><div class="dual__fill" data-fill></div></div>
<input class="slider slider--ghost" type="range" min="0" max="10000" step="500" value="2000" data-lo aria-label="下限価格">
<input class="slider slider--ghost" type="range" min="0" max="10000" step="500" value="7000" data-hi aria-label="上限価格">
</div>
</div>
</div>
【CSS】
:root{
--bg:#0f1729;
--card:#172036;
--accent:#22d3ee;
--accent2:#a78bfa;
--text:#e6edf7;
--muted:#8b97b5;
--track:#2a3553;
}
*{box-sizing:border-box}
body{
margin:0;min-height:100vh;
display:grid;place-items:center;padding:26px 16px;
font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
background:radial-gradient(700px 360px at 50% -10%,#1b2c4a,transparent),var(--bg);
}
.rng{
width:min(420px,100%);
background:var(--card);border:1px solid #25304e;border-radius:18px;
padding:26px 24px;
}
.rng__title{margin:0 0 22px;font-size:1.15rem;display:flex;align-items:center;gap:10px}
.rng__title::before{
content:"";width:8px;height:20px;border-radius:4px;
background:linear-gradient(var(--accent),var(--accent2));
}
.rng__group{margin-bottom:26px}
.rng__group:last-child{margin-bottom:0}
.rng__label{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px}
.rng__label span:first-child{color:var(--muted);font-size:.9rem}
.rng__val{font-weight:700;color:var(--accent);font-variant-numeric:tabular-nums}
/* 共通スライダー外観(クロスブラウザ) */
.slider{
-webkit-appearance:none;appearance:none;width:100%;height:6px;
background:var(--track);border-radius:999px;outline:none;margin:0;
/* 進捗をグラデで表現(--p は0〜100) */
background-image:linear-gradient(90deg,var(--accent),var(--accent2));
background-repeat:no-repeat;
background-size:calc(var(--p,60) * 1%) 100%;
}
.slider::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:20px;height:20px;border-radius:50%;cursor:pointer;
background:#fff;border:3px solid var(--accent);
box-shadow:0 4px 10px rgba(0,0,0,.4);transition:transform .15s;
}
.slider::-webkit-slider-thumb:active{transform:scale(1.18)}
.slider::-moz-range-thumb{
width:18px;height:18px;border-radius:50%;cursor:pointer;
background:#fff;border:3px solid var(--accent);
}
.slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--accent2);outline-offset:2px}
/* 2ハンドル:透明スライダーを重ねる方式 */
.dual{position:relative;height:24px;display:flex;align-items:center}
.dual__track{
position:absolute;left:0;right:0;height:6px;border-radius:999px;background:var(--track);
}
.dual__fill{
position:absolute;height:6px;border-radius:999px;
background:linear-gradient(90deg,var(--accent),var(--accent2));
}
.slider--ghost{
position:absolute;left:0;right:0;width:100%;background:none;background-image:none;
pointer-events:none;height:24px;
}
.slider--ghost::-webkit-slider-thumb{pointer-events:auto}
.slider--ghost::-moz-range-thumb{pointer-events:auto}
@media (prefers-reduced-motion:reduce){.slider::-webkit-slider-thumb{transition:none}}
【JavaScript】
// 単一スライダー:進捗グラデと値表示を更新
const bright = document.getElementById('bright');
const brightVal = document.getElementById('brightVal');
const updateSingle = () => {
if (!bright || !brightVal) return;
bright.style.setProperty('--p', bright.value); // 0-100をそのまま%に
brightVal.textContent = bright.value + '%';
};
bright?.addEventListener('input', updateSingle);
updateSingle();
// 2ハンドル価格帯スライダー
const dual = document.querySelector('[data-dual]');
if (dual) {
const lo = dual.querySelector('[data-lo]');
const hi = dual.querySelector('[data-hi]');
const fill = dual.querySelector('[data-fill]');
const out = document.getElementById('priceVal');
const MIN = +lo.min, MAX = +lo.max;
const yen = (n) => '¥' + Number(n).toLocaleString('ja-JP');
const updateDual = () => {
let loV = +lo.value, hiV = +hi.value;
// ハンドルの追い越しを防ぐ
if (loV > hiV) { [loV, hiV] = [hiV, loV]; lo.value = loV; hi.value = hiV; }
const span = MAX - MIN || 1;
const lp = ((loV - MIN) / span) * 100;
const hp = ((hiV - MIN) / span) * 100;
fill.style.left = lp + '%';
fill.style.width = (hp - lp) + '%';
if (out) out.textContent = `${yen(loV)} – ${yen(hiV)}`;
};
lo.addEventListener('input', updateDual);
hi.addEventListener('input', updateDual);
updateDual();
}
# 外部ライブラリ
なし(追加ライブラリ不要)
# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。