レンジスライダー(単一・価格帯)

進捗グラデ付きの単一スライダーと、2ハンドルの価格帯スライダー。input[type=range]の装飾で、フィルターや絞り込みに使えます。

#css#javascript#forms

ライブデモ

使用例(お題: 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。