Inputs

Price Range

Диапазон двумя нативными ползунками: границы упираются друг в друга, поэтому «от» никогда не становится больше «до».

  • range
  • filter
  • price
  • slider

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/range-001

Цена4 00014 000

020 000

Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «range-001» (Price Range) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/range-001.json

Registry item: https://vibeui.ru/r/range-001.json
Ставится в: components/vibeui/range-001.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

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

## 2. Что это
Диапазон двумя нативными ползунками: границы упираются друг в друга, поэтому «от» никогда не становится больше «до».

Диапазон цены: две границы на нативных ползунках, закрашенный отрезок между ними и подписи краёв. Ноль зависимостей, один файл.

## 3. Как использовать
import { Range001 } from "@/components/vibeui/range-001"

<Range001 label="Цена" min={0} max={20000} step={500} defaultFrom={4000} defaultTo={14000} />

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-range-001-* и не подменять её токенами вашей темы
- два нативных ползунка: свой драг пришлось бы учить клавиатуре и касаниям
- pointer-events только на ручках — иначе верхний ползунок съедает клики по нижнему
- упор границ друг в друга: перепрыгивание даёт пустую выборку
- aria-label на каждой границе: два одинаковых ползунка неразличимы на слух
- текущие значения в заголовке, а не только положение ручек

## 6. Можно менять
- label, min, max и step
- defaultFrom и defaultTo
- unit — единицу измерения
- акцент через проп accent

## 7. Правила
- Ручки на одинаковых значениях наложатся: минимальный зазор задаёт step.
- Стилизация ручек идёт через префиксы: правила ::-webkit- и ::-moz- нельзя объединять в один селектор.
- Для точных сумм рядом с фильтром оставьте два числовых поля.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/range-001.json
https://vibeui.ru/r/range-001.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-range-001-*. Клиентский: "use client" ради двух границ. Ползунки лежат друг на друге: дорожка не ловит события, а ручки ловят, поэтому обе доступны мышью. Закрашенный отрезок рисуется градиентом по двум переменным процента.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

"use client"

import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Range001Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue" | "onChange"
> & {
  label?: string
  min?: number
  max?: number
  step?: number
  defaultFrom?: number
  defaultTo?: number
  unit?: string
  accent?: string
}

// Идея компонента: диапазон двумя нативными ползунками. Один ползунок не умеет
// хранить две границы, а свой драг пришлось бы учить клавиатуре и касаниям.
// Ползунки лежат друг на друге: дорожка отключена для событий, а ручки — нет,
// поэтому обе доступны мышью. Границы не перепрыгивают: каждая упирается в
// соседнюю, иначе «от» становится больше «до» и фильтр возвращает пустоту.
const STYLES = `
:where([data-vibeui-block="range-001"]){
--vibeui-range-001-bg:oklch(1 0 0);
--vibeui-range-001-fg:oklch(0.24 0.014 265);
--vibeui-range-001-muted:oklch(0.56 0.014 265);
--vibeui-range-001-border:oklch(0.9 0.006 265);
--vibeui-range-001-track:oklch(0.92 0.006 265);
--vibeui-range-001-accent:oklch(0.55 0.2 262);
--vibeui-range-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-range-001-from:0%;
--vibeui-range-001-to:100%;
}
[data-vibeui-block="range-001"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-range-001-bg);
border:1px solid var(--vibeui-range-001-border);border-radius:0.875rem;
font-family:var(--vibeui-range-001-font);color:var(--vibeui-range-001-fg);
}
[data-vibeui-block="range-001"] [data-part="head"]{
display:flex;align-items:baseline;justify-content:space-between;gap:1rem;
margin:0;font-size:0.875rem;
}
[data-vibeui-block="range-001"] [data-part="value"]{font-weight:650;font-variant-numeric:tabular-nums}
[data-vibeui-block="range-001"] [data-part="rail"]{
position:relative;height:1.25rem;
}
/* Дорожка одна на два ползунка: закрашен отрезок между границами. */
[data-vibeui-block="range-001"] [data-part="rail"]::before{
content:"";position:absolute;left:0;right:0;top:0.4375rem;height:0.375rem;
border-radius:9999px;
background:linear-gradient(to right,
var(--vibeui-range-001-track) var(--vibeui-range-001-from),
var(--vibeui-range-001-accent) var(--vibeui-range-001-from),
var(--vibeui-range-001-accent) var(--vibeui-range-001-to),
var(--vibeui-range-001-track) var(--vibeui-range-001-to));
}
[data-vibeui-block="range-001"] input{
position:absolute;left:0;top:0;width:100%;height:1.25rem;margin:0;
appearance:none;background:none;pointer-events:none;
}
[data-vibeui-block="range-001"] input::-webkit-slider-runnable-track{background:none;height:0.375rem}
[data-vibeui-block="range-001"] input::-moz-range-track{background:none;height:0.375rem}
/* Дорожка событий не ловит, ручки ловят: иначе верхний ползунок съедает клики. */
[data-vibeui-block="range-001"] input::-webkit-slider-thumb{
appearance:none;pointer-events:auto;cursor:pointer;margin-top:-0.3125rem;
width:1rem;height:1rem;border-radius:9999px;
background:var(--vibeui-range-001-bg);border:2px solid var(--vibeui-range-001-accent);
box-shadow:0 1px 3px oklch(0.2 0.02 265 / 25%);
}
[data-vibeui-block="range-001"] input::-moz-range-thumb{
pointer-events:auto;cursor:pointer;box-sizing:border-box;
width:1rem;height:1rem;border-radius:9999px;
background:var(--vibeui-range-001-bg);border:2px solid var(--vibeui-range-001-accent);
}
[data-vibeui-block="range-001"] input:focus-visible{outline:2px solid var(--vibeui-range-001-accent);outline-offset:4px;border-radius:0.5rem}
[data-vibeui-block="range-001"] [data-part="scale"]{
display:flex;justify-content:space-between;margin:0;
font-size:0.6875rem;color:var(--vibeui-range-001-muted);font-variant-numeric:tabular-nums;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="range-001"] *{animation:none!important;transition:none!important}}
`

/**
 * Диапазон двумя нативными ползунками: границы не перепрыгивают друг друга.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Range001({
  label = "Цена",
  min = 0,
  max = 20000,
  step = 500,
  defaultFrom = 4000,
  defaultTo = 14000,
  unit = " ₽",
  accent,
  className,
  style,
  ...props
}: Range001Props) {
  const [from, setFrom] = useState(defaultFrom)
  const [to, setTo] = useState(defaultTo)
  const percent = (value: number) => `${((value - min) / (max - min)) * 100}%`

  const palette = {
    "--vibeui-range-001-from": percent(from),
    "--vibeui-range-001-to": percent(to),
    ...(accent ? { "--vibeui-range-001-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-range-001" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="range-001"
        className={className}
        style={palette}
      >
        <p data-part="head">
          {label}
          <span data-part="value">
            {from.toLocaleString("ru-RU")}
            {unit} — {to.toLocaleString("ru-RU")}
            {unit}
          </span>
        </p>
        <div data-part="rail">
          <input
            type="range"
            min={min}
            max={max}
            step={step}
            value={from}
            aria-label={`${label}: от`}
            onChange={(event) =>
              setFrom(Math.min(Number(event.target.value), to - step))
            }
          />
          <input
            type="range"
            min={min}
            max={max}
            step={step}
            value={to}
            aria-label={`${label}: до`}
            onChange={(event) =>
              setTo(Math.max(Number(event.target.value), from + step))
            }
          />
        </div>
        <p data-part="scale">
          <span>
            {min.toLocaleString("ru-RU")}
            {unit}
          </span>
          <span>
            {max.toLocaleString("ru-RU")}
            {unit}
          </span>
        </p>
      </div>
    </>
  )
}