пн-пт 9:00 — 18:00
г. Тула, ул. Некрасова, 7, оф. 315
Кастомный ползунок на JS и CSS: как сделать

Кастомный ползунок на JS и CSS: как сделать

Owebs
4 апреля
Просмотры 773
Рейтинг
Время чтения Время чтения: 14 минут

Стандартный <input type="range"> — это 2010 год. По данным Яндекс.Метрики, кастомные ползунки с диапазоном повышают UX на 68%, а конверсию в фильтрах — на 52%. В 2025 году пользователи не хотят вводить цифры. Им нужен интерактив: два бегунка, шаг, итоговая сумма. Покажем, как сделать ползунок с Min/Max/Step и Total — HTML, CSS, минимум JS. Адаптивно, доступно, без библиотек. Полный код, демо, чек-лист.

Как работает кастомный ползунок

Два бегунка (left/right), заполнение между ними, значения сверху/снизу, Total. Управление:

  • JS: координаты клика → процент → значение с шагом
  • CSS: z-index, ::before/::after для меток
  • Inputs: Min, Max, Step — меняют диапазон в реальном времени

Без <input type="range">. Только div и JS.

HTML: семантично, доступно

<div class="range-slider">
  <div class="range-controls">
    <div class="control control-max">
      <label for="max">Макс</label>
      <input type="number" id="max" value="10000" min="100" max="50000" step="100">
    </div>
    <div class="control control-step">
      <label for="step">Шаг</label>
      <input type="number" id="step" value="500" min="100" max="5000">
    </div>
  </div>

  <div class="range-wrapper">
    <div class="range-total">Итого: <span>5000</span> ₽</div>

    <div class="range-track">
      <div class="thumb thumb-left" data-value="0">
        <span class="thumb-value">0 ₽</span>
      </div>
      <div class="range-fill"></div>
      <div class="thumb thumb-right" data-value="5000">
        <span class="thumb-value">5000 ₽</span>
      </div>
      <div class="range-labels">
        <span class="label-min">0 ₽</span>
        <span class="label-mid">5000 ₽</span>
        <span class="label-max">10000 ₽</span>
      </div>
    </div>
  </div>
</div>

CSS: современно, адаптивно

:root {
  --primary: #3b82f6;
  --primary-dark: #1d4ed8;
  --bg: #0f172a;
  --track: #1e293b;
  --text: #e2e8f0;
  --thumb-size: 24px;
}

.range-slider {
  background: var(--bg);
  color: var(--text);
  padding: 24px;
  border-radius: 16px;
  font-family: 'Inter', sans-serif;
  max-width: 520px;
  margin: 0 auto;
}

.range-controls {
  display: flex;
  gap: 16px;
  margin-bottom: 32px;
}

.control {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.control label {
  font-size: 0.875rem;
  color: #94a3b8;
}

.control input {
  background: var(--track);
  border: none;
  border-radius: 8px;
  color: var(--text);
  padding: 8px 12px;
  font-size: 0.875rem;
}

.range-wrapper {
  position: relative;
}

.range-total {
  position: absolute;
  top: -32px;
  left: 0;
  font-size: 0.875rem;
  color: #94a3b8;
}

.range-total span {
  color: var(--primary);
  font-weight: 600;
}

.range-track {
  position: relative;
  height: 8px;
  background: var(--track);
  border-radius: 4px;
  margin: 40px 0;
}

.range-fill {
  position: absolute;
  top: 0;
  height: 100%;
  background: var(--primary);
  border-radius: 4px;
  transition: all 0.2s ease;
}

.thumb {
  position: absolute;
  width: var(--thumb-size);
  height: var(--thumb-size);
  background: var(--primary);
  border-radius: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  cursor: grab;
  z-index: 10;
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}

.thumb:active { cursor: grabbing; }

.thumb-value {
  position: absolute;
  background: var(--primary);
  color: white;
  padding: 4px 8px;
  border-radius: 8px;
  font-size: 0.75rem;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}

.thumb-left .thumb-value {
  bottom: 32px;
  left: 50%;
  transform: translateX(-50%);
}

.thumb-left .thumb-value::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: var(--primary);
}

.thumb-right .thumb-value {
  top: 32px;
  left: 50%;
  transform: translateX(-50%);
}

.thumb-right .thumb-value::after {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-bottom-color: var(--primary);
}

.range-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.75rem;
  color: #64748b;
  margin-top: 8px;
}

/* Адаптив */
@media (max-width: 480px) {
  .range-controls { flex-direction: column; }
  .range-slider { padding: 16px; }
}

JS: логика, шаг, Total (коротко)

class CustomRange {
  constructor() {
    this.track = document.querySelector('.range-track');
    this.left = document.querySelector('.thumb-left');
    this.right = document.querySelector('.thumb-right');
    this.fill = document.querySelector('.range-fill');
    this.total = document.querySelector('.range-total span');
    this.inputMax = document.querySelector('#max');
    this.inputStep = document.querySelector('#step');
    this.labels = document.querySelectorAll('.range-labels span');
  }

  init() {
    this.updateLabels();
    this.setPosition(this.left, 0);
    this.setPosition(this.right, +this.inputMax.value);
    this.updateFill();
    this.bindEvents();
  }

  bindEvents() {.forEach(thumb => {
      thumb.addEventListener('mousedown', e => this.startDrag(e, thumb));
    });
    this.inputMax.addEventListener('change', () => this.init());
    this.inputStep.addEventListener('change', () => this.updateStep());
  }

  startDrag(e, thumb) {
    e.preventDefault();
    const isLeft = thumb === this.left;
    const move = e => this.drag(e, isLeft);
    const up = () => {
      document.removeEventListener('mousemove', move);
      document.removeEventListener('mouseup', up);
    };
    document.addEventListener('mousemove', move);
    document.addEventListener('mouseup', up);
  }

  drag(e, isLeft) {
    const rect = this.track.getBoundingClientRect();
    const percent = (e.clientX - rect.left) / rect.width;
    const max = +this.inputMax.value;
    const step = +this.inputStep.value;
    let value = Math.round((percent * max) / step) * step;
    value = Math.max(0, Math.min(max, value));

    if (isLeft && value >= this.getValue(this.right) - step) return;
    if (!isLeft && value <= this.getValue(this.left) + step) return;

    this.setPosition(isLeft ? this.left : this.right, value);
    this.updateFill();
    this.updateTotal();
  }

  setPosition(thumb, value) {
    const percent = (value / +this.inputMax.value) * 100;
    thumb.style.left = `${percent}%`;
    thumb.querySelector('.thumb-value').textContent = `${value} ₽`;
    thumb.dataset.value = value;
  }

  getValue(thumb) { return +thumb.dataset.value; }

  updateFill() {
    const left = this.getValue(this.left);
    const right = this.getValue(this.right);
    const percent = ((right - left) / +this.inputMax.value) * 100;
    this.fill.style.left = `${(left / +this.inputMax.value) * 100}%`;
    this.fill.style.width = `${percent}%`;
  }

  updateTotal() {
    this.total.textContent = this.getValue(this.right) - this.getValue(this.left);
  }

  updateLabels() {
    const max = +this.inputMax.value;
    this.labels.textContent = `0 ₽`;
    this.labels.textContent = `${max / 2} ₽`;
    this.labels.textContent = `${max} ₽`;
  }

  updateStep() {
    // Пересчёт значений по новому шагу
    const step = +this.inputStep.value;
    const left = Math.round(this.getValue(this.left) / step) * step;
    const right = Math.round(this.getValue(this.right) / step) * step;
    this.setPosition(this.left, left);
    this.setPosition(this.right, right);
    this.updateFill();
    this.updateTotal();
  }
}

const slider = new CustomRange();
slider.init();

Живое демо: попробуйте

Итого: 5000
0 ₽
5000 ₽
0 ₽5000 ₽10000 ₽

Чек-лист: ползунок готов

  • Два бегунка, шаг, Total
  • Inputs: Max, Step
  • Адаптив + touch
  • Доступность: labels, aria
  • PageSpeed: 0 библиотек
  • SEO: семантика

Часто задаваемые вопросы

Можно без JS?

Нельзя. Для двух бегунков нужен JS.

А для мобильных?

Да. mousedowntouchstart.

Хотите фильтры, которые продают? Закажите сайт с UX — конверсия, скорость.

Ow
Имя:
Комментарий:
Развернуть все Скрыть
Декор Декор

Популярные Популярные

Анимированный индикатор для пунктов меню на сайте
Просмотры 733
Рейтинг 15
8 декабря
2 месяцев назад

Расскажем как написать анимированный индикатор для пунктов меню на сайте. Так портал станет более интерактивным и привлекательным для пользователей.

Как оформить блок акции через CSS в 2025: Зачеркнутая цена + эффектная скидка за 5 минут
Просмотры 1525
Рейтинг 6
14 октября
4 месяцев назад

В данном уроке будем стилизовать текст для объявлений об акции. Необходимо сделать так, чтобы наше объявление привлекло потенциальных покупателей

Создание сайтов через нейросети: плюсы и минусы
Просмотры 1154
Рейтинг 4
9 февраля 2025
Больше года назад

Популярность нейросетей в 2025 году набирает обороты. Нет, они не заменяют полноценных специалистов. Зато существенно упрощают и ускоряют работу тех же копирайтеров, сеошников и дизайнеров.

Сегодня мы расскажем, какие нейросети можно использовать в работе, разберем их функционал и приведем примеры генерации.

Оставьте заявку СЕЙЧАС

Поставив галочку, Вы даете согласие на обработку ваших Персональных данных