Range Slider — レンジスライダー(単一・デュアル)

フォーム入力 初級

このコンポーネントについて

ドラッグ操作で数値を直感的に入力できるレンジスライダーコンポーネントです。 音量・不透明度・フォントサイズのような単一値入力から、ECサイトの価格帯フィルターのような最小・最大の範囲指定まで幅広く使われます。

このページでは、1軸で値を指定する単一スライダーと、2つのつまみで範囲を指定するデュアルスライダーの2パターンを提供します。 どちらも「決定」ボタンで確定値を表示するUIになっています。

  • リアルタイム値表示 — ドラッグ中に現在値が即座に更新される
  • 決定ボタン — ボタンをクリックした時点の値を結果エリアに表示する
  • CSSカスタムスライダー — ブラウザデフォルトUIを上書きし、デザインを統一できるスタイル
  • デュアルスライダー(min/max) — 2つのつまみで範囲を指定するパターン。下限が上限を追い越さないよう制約付き
  • クロスブラウザ対応 — ChromeとFirefox両方のベンダープレフィックスに対応したCSS

実装のポイント・注意点

input[type="range"] のデフォルトUIはブラウザごとに大きく異なります。 統一したデザインにするには appearance: none(Chromeは -webkit-appearance: none)でリセットし、 ::-webkit-slider-thumb(Chrome/Safari)と ::-moz-range-thumb(Firefox)の両方にスタイルを記述する必要があります。 Firefoxでは ::-moz-range-thumbborder: none を指定しないとデフォルトの枠線が残るため注意してください。

デュアルスライダーは2つの input[type="range"]position: absolute で重ねて実装します。 トラック全体には pointer-events: none を指定して、つまみ(::-webkit-slider-thumb)だけに pointer-events: all を設定することで、 両方のつまみが正しく操作できるようになります。 下限つまみが最大値に近づいたとき、z-index を動的に切り替えて前面に出す処理もJavaScriptで対応しています。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

Pattern 1 — 単一レンジ

50

Pattern 2 — デュアルレンジ(min/max)

¥10,000 〜 ¥80,000

サンプルソース

3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>レンジスライダー サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<!-- Pattern 1: 単一レンジ -->
<div class="range-wrapper" id="wrapper1">
  <div class="range-header">
    <label class="range-label" for="volumeRange">音量</label>
    <span class="range-value" id="volumeValue">50</span>
  </div>
  <input type="range" id="volumeRange" class="range-input" min="0" max="100" value="50">
  <div class="range-actions">
    <button type="button" class="range-confirm-btn" id="volumeConfirmBtn">決定</button>
  </div>
  <p class="range-result" id="volumeResult" hidden></p>
</div>

<!-- Pattern 2: デュアルレンジ(min/max) -->
<div class="range-wrapper" id="wrapper2">
  <div class="range-header">
    <label class="range-label">価格帯</label>
    <span class="range-value" id="priceValue">¥10,000 〜 ¥80,000</span>
  </div>
  <div class="dual-range-track">
    <input type="range" id="priceMin"
      class="range-input range-input--dual range-input--min"
      min="0" max="100000" step="1000" value="10000">
    <input type="range" id="priceMax"
      class="range-input range-input--dual range-input--max"
      min="0" max="100000" step="1000" value="80000">
  </div>
  <div class="range-actions">
    <button type="button" class="range-confirm-btn" id="priceConfirmBtn">決定</button>
  </div>
  <p class="range-result" id="priceResult" hidden></p>
</div>

<script src="./script.js"></script>
</body>
</html>
/* === レンジスライダー ===
   色を変えたいときは :root の変数を書き換えるだけでOKです */
:root {
  --color-primary:      #2B7FE8; /* つまみ・強調色 */
  --color-track:        #D0D7E0; /* トラック背景色 */
  --color-thumb-shadow: rgba(43, 127, 232, 0.25); /* つまみのフォーカスリング */
}

*, *::before, *::after { box-sizing: border-box; }
body {
  font-family: sans-serif;
  padding: 24px;
  max-width: 480px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.range-wrapper { width: 100%; }

.range-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.range-label {
  font-size: 14px;
  font-weight: 600;
  color: #1E2A3A;
}

/* ドラッグ中にリアルタイム更新される現在値 */
.range-value {
  font-size: 14px;
  font-weight: 700;
  color: var(--color-primary);
}

/* ブラウザデフォルトのスライダーUIをリセット */
.range-input {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 6px;
  border-radius: 3px;
  background: var(--color-track);
  outline: none;
  cursor: pointer;
}

/* Chrome / Safari のつまみ */
.range-input::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--color-primary);
  cursor: grab;
  box-shadow: 0 0 0 3px var(--color-thumb-shadow);
  transition: box-shadow 0.15s;
}

.range-input::-webkit-slider-thumb:active {
  cursor: grabbing;
  box-shadow: 0 0 0 5px var(--color-thumb-shadow);
}

/* Firefox のつまみ(border: none がないとデフォルト枠が残る) */
.range-input::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: var(--color-primary);
  cursor: grab;
  box-shadow: 0 0 0 3px var(--color-thumb-shadow);
}

/* === デュアルレンジ === */

/* 2つのスライダーを重ねるコンテナ */
.dual-range-track {
  position: relative;
  height: 24px;
}

/* 2つの透明inputの背後にトラックを描く */
.dual-range-track::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 6px;
  transform: translateY(-50%);
  background: var(--color-track);
  border-radius: 3px;
  pointer-events: none;
}

/* 2つのスライダーを同じ位置に重ねる */
.range-input--dual {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: transparent; /* トラックは親コンテナのスタイルに任せる */
  pointer-events: none;    /* トラック全体のクリックを無効化 */
}

.range-input--min { z-index: 1; }
.range-input--max { z-index: 2; }

/* つまみだけクリックできるようにする */
.range-input--dual::-webkit-slider-thumb { pointer-events: all; }
.range-input--dual::-moz-range-thumb     { pointer-events: all; }

/* === 決定ボタン・結果エリア === */

.range-actions {
  margin-top: 16px;
  display: flex;
  justify-content: flex-end;
}

.range-confirm-btn {
  padding: 8px 20px;
  font-size: 14px;
  font-family: sans-serif;
  color: #fff;
  background: var(--color-primary);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.range-confirm-btn:hover { background: #1a6fd4; }

/* hidden 属性が外れると表示される */
.range-result {
  margin-top: 12px;
  padding: 10px 14px;
  background: #F4F6F9;
  border-radius: 6px;
  font-size: 14px;
  color: #1E2A3A;
}

@media (max-width: 480px) {
  body { padding: 16px; }
}
// ===== Pattern 1: 単一レンジ =====
var volumeRange      = document.getElementById('volumeRange');
var volumeValueEl    = document.getElementById('volumeValue');
var volumeConfirmBtn = document.getElementById('volumeConfirmBtn');
var volumeResult     = document.getElementById('volumeResult');

// ドラッグ中に現在値をリアルタイム表示
volumeRange.addEventListener('input', function () {
  volumeValueEl.textContent = volumeRange.value;
});

// 決定ボタン: その時点の値を結果エリアに表示する
volumeConfirmBtn.addEventListener('click', function () {
  volumeResult.textContent = '音量: ' + volumeRange.value;
  volumeResult.hidden = false;
});

// ===== Pattern 2: デュアルレンジ =====
var priceMin        = document.getElementById('priceMin');
var priceMax        = document.getElementById('priceMax');
var priceValueEl    = document.getElementById('priceValue');
var priceConfirmBtn = document.getElementById('priceConfirmBtn');
var priceResult     = document.getElementById('priceResult');
var STEP = 1000;

// 数値を「¥10,000」形式にフォーマットする
function formatPrice(val) {
  return '¥' + parseInt(val, 10).toLocaleString('ja-JP');
}

function updateDualRange() {
  var minVal = parseInt(priceMin.value, 10);
  var maxVal = parseInt(priceMax.value, 10);

  // 下限が上限を超えないよう制約する
  if (minVal >= maxVal) {
    priceMin.value = maxVal - STEP;
    minVal = maxVal - STEP;
  }

  // 下限が最大値付近のとき、下限側のz-indexを上げて操作できるようにする
  priceMin.style.zIndex = minVal > 90000 ? '3' : '1';

  // 価格表示を更新
  priceValueEl.textContent = formatPrice(priceMin.value) + ' 〜 ' + formatPrice(priceMax.value);
}

priceMin.addEventListener('input', updateDualRange);
priceMax.addEventListener('input', updateDualRange);

// 決定ボタン: その時点の範囲を結果エリアに表示する
priceConfirmBtn.addEventListener('click', function () {
  priceResult.textContent = '価格帯: ' + formatPrice(priceMin.value) + ' 〜 ' + formatPrice(priceMax.value);
  priceResult.hidden = false;
});

AI用プロンプト

各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。色やレンジ幅の変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

Pattern 1 — 単一レンジ

# レンジスライダー(単一)作成依頼

## 概要
ドラッグで数値を入力できる単一レンジスライダーを作成してください。音量や不透明度など1軸の値入力に使います。

## 要件
- スライダーをドラッグすると現在値がリアルタイムで更新される
- ブラウザデフォルトUIをCSSで上書きし、統一されたデザインにする
- ラベルと現在値を横並びで表示する
- 「決定」ボタンをクリックすると、その時点の値を結果エリアに表示する

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
input[type="range"]のinputイベントで値表示をリアルタイム更新します。CSSは::-webkit-slider-thumbと::-moz-range-thumbの両方に対応してください。決定ボタンのclickイベントで結果エリア(初期hidden)にtextContentで値を書き込み、hidden属性を外して表示します。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。

Pattern 2 — デュアルレンジ(min/max)

# レンジスライダー(デュアル min/max)作成依頼

## 概要
最小値と最大値を独立したつまみで指定できるデュアルレンジスライダーを作成してください。価格帯フィルターなどの範囲指定入力に使います。

## 要件
- 2つのinput[type="range"]を同一のトラック上に重ねて配置する
- 下限つまみが上限つまみを超えないよう、上限つまみが下限つまみを下回らないよう制約を設ける
- 「¥10,000 〜 ¥80,000」のように選択中の範囲を桁区切り付きで表示する
- 「決定」ボタンをクリックすると、その時点の範囲を結果エリアに表示する

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要

## 動作詳細
position: absoluteで2つのinputを重ねます。トラックにpointer-events: noneを設定し、つまみ(::-webkit-slider-thumb)にのみpointer-events: allを設定して両方のつまみを操作できるようにします。どちらのinputのinputイベントでも両方の値を読み取り、相互制約のチェックと表示更新を行います。価格表示はtoLocaleString('ja-JP')で桁区切りを付けます。決定ボタンで結果エリアに確定値を表示します。

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。