Inputs

Slider With Number

A slider and a number field side by side: the mouse estimates, the keyboard enters an exact figure, and both inputs hold one value.

  • slider
  • number
  • input
  • form

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/slider-008?lang=en

k ₽

от 5 до 300 k ₽, шаг 5

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "slider-008" (Slider With Number) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/slider-008.json

Registry item: https://vibeui.ru/r/slider-008.json
Installs to: components/vibeui/slider-008.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A slider and a number field side by side: the mouse estimates, the keyboard enters an exact figure, and both inputs hold one value.

A slider with a number field: roughly by mouse, precisely by keyboard. One file, zero dependencies, a client component.

## 3. How to use it
import { Slider008 } from "@/components/vibeui/slider-008"

<Slider008
  label="Advertising budget"
  defaultValue={120}
  unit="k ₽"
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-slider-008-* palette — do not swap it for your theme tokens
- the deferred clamping: clamping on every keystroke makes numbers starting with a small digit impossible to type
- the separate draft string — it is what lets the field survive an unfinished entry
- a distinct accessible name on the number field: two doors into one value must not sound identical
- tabular figures and the fixed field width — otherwise it twitches on every digit
- the component's own light surface — without it the dark text disappears on a dark page

## 6. You may change
- the caption through label and the bounds through min and max
- the starting value through defaultValue
- the unit of measure through unit
- the increment through step

## 7. Rules
- Typed input is not snapped to the step: an off-grid value stays as typed, so round it in commit if that matters.
- The number spinners are hidden on purpose; the up and down arrow keys still work.
- An empty field restores the previous value on blur rather than zero: a budget must not drop to nothing by accident.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/slider-008.json
https://vibeui.ru/r/slider-008.json

Клиентский компонент: useState на значение и отдельно на строку ввода. Поле не приводится к диапазону на каждом нажатии — иначе «10» невозможно набрать, когда минимум равен 100; зажим по границам происходит на blur и по Enter. Стрелки числового поля убраны вендорными псевдоэлементами, цифры табличные, ширина фиксирована. Заливка дорожки — градиент по проценту. Палитра в --vibeui-slider-008-*.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

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

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

// Идея компонента: ползунок и поле числа — две двери в одно значение.
// Ползунком удобно прикидывать, полем — вводить точную цифру из брифа.
// Поле не «чинит» ввод на каждом нажатии: пока в нём печатают, значение
// живёт строкой, а к диапазону приводится на blur — иначе «10» невозможно
// набрать, если минимум равен 100.
const STYLES = `
:where([data-vibeui-block="slider-008"]){
--vibeui-slider-008-bg:oklch(1 0 0);
--vibeui-slider-008-fg:oklch(0.22 0.014 265);
--vibeui-slider-008-muted:oklch(0.55 0.014 265);
--vibeui-slider-008-border:oklch(0.9 0.006 265);
--vibeui-slider-008-field:oklch(0.985 0.002 265);
--vibeui-slider-008-track:oklch(0.92 0.006 265);
--vibeui-slider-008-accent:oklch(0.52 0.16 195);
--vibeui-slider-008-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-slider-008-fill:50%;
}
[data-vibeui-block="slider-008"]{
display:flex;flex-direction:column;gap:0.625rem;
width:100%;max-width:21rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-slider-008-bg);
border:1px solid var(--vibeui-slider-008-border);border-radius:0.875rem;
font-family:var(--vibeui-slider-008-font);color:var(--vibeui-slider-008-fg);
}
[data-vibeui-block="slider-008"] [data-part="label"]{font-size:0.8125rem;font-weight:600}
[data-vibeui-block="slider-008"] [data-part="row"]{display:flex;align-items:center;gap:0.875rem}
[data-vibeui-block="slider-008"] [data-part="rail"]{flex:1 1 auto;min-width:0;display:flex}
[data-vibeui-block="slider-008"] [data-part="range"]{
appearance:none;width:100%;height:1.25rem;margin:0;background:none;cursor:pointer;
}
[data-vibeui-block="slider-008"] [data-part="range"]::-webkit-slider-runnable-track{
height:0.375rem;border-radius:9999px;
background:linear-gradient(to right,var(--vibeui-slider-008-accent) var(--vibeui-slider-008-fill),var(--vibeui-slider-008-track) var(--vibeui-slider-008-fill));
}
[data-vibeui-block="slider-008"] [data-part="range"]::-moz-range-track{
height:0.375rem;border-radius:9999px;
background:linear-gradient(to right,var(--vibeui-slider-008-accent) var(--vibeui-slider-008-fill),var(--vibeui-slider-008-track) var(--vibeui-slider-008-fill));
}
[data-vibeui-block="slider-008"] [data-part="range"]::-webkit-slider-thumb{
appearance:none;margin-top:-0.3125rem;
width:1rem;height:1rem;border-radius:9999px;
background:var(--vibeui-slider-008-accent);border:3px solid var(--vibeui-slider-008-bg);
box-shadow:0 1px 4px oklch(0.2 0.02 265 / 30%);
}
[data-vibeui-block="slider-008"] [data-part="range"]::-moz-range-thumb{
width:1rem;height:1rem;border-radius:9999px;box-sizing:border-box;
background:var(--vibeui-slider-008-accent);border:3px solid var(--vibeui-slider-008-bg);
}
[data-vibeui-block="slider-008"] [data-part="range"]:focus-visible{outline:2px solid var(--vibeui-slider-008-accent);outline-offset:4px;border-radius:0.5rem}
/* Поле числа: стрелки убраны, ширина фиксирована и цифры моноширинные —
   иначе поле дёргается на каждом разряде. */
[data-vibeui-block="slider-008"] [data-part="field"]{
display:flex;align-items:center;gap:0.25rem;flex:none;
padding:0 0.5rem;height:2.25rem;border-radius:0.5rem;
border:1px solid var(--vibeui-slider-008-border);
background:var(--vibeui-slider-008-field);
transition:border-color .16s ease,box-shadow .16s ease;
}
[data-vibeui-block="slider-008"] [data-part="field"]:focus-within{
border-color:var(--vibeui-slider-008-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-slider-008-accent) 20%,transparent);
}
[data-vibeui-block="slider-008"] [data-part="number"]{
appearance:none;-moz-appearance:textfield;
width:3rem;border:0;padding:0;background:none;color:inherit;
font:inherit;font-size:0.9375rem;font-weight:650;font-variant-numeric:tabular-nums;
text-align:right;
}
[data-vibeui-block="slider-008"] [data-part="number"]:focus{outline:none}
[data-vibeui-block="slider-008"] [data-part="number"]::-webkit-outer-spin-button,
[data-vibeui-block="slider-008"] [data-part="number"]::-webkit-inner-spin-button{
-webkit-appearance:none;margin:0;
}
[data-vibeui-block="slider-008"] [data-part="unit"]{font-size:0.75rem;color:var(--vibeui-slider-008-muted)}
[data-vibeui-block="slider-008"] [data-part="hint"]{
margin:0;font-size:0.6875rem;color:var(--vibeui-slider-008-muted);font-variant-numeric:tabular-nums;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="slider-008"] *{animation:none!important;transition:none!important}}
`

/**
 * Ползунок с полем числа: приблизительно мышью, точно — с клавиатуры.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Slider008({
  label = "Бюджет на рекламу",
  min = 5,
  max = 300,
  step = 5,
  defaultValue = 60,
  unit = "тыс. ₽",
  accent,
  className,
  style,
  ...props
}: Slider008Props) {
  const id = useId()
  const [value, setValue] = useState(defaultValue)
  const [draft, setDraft] = useState(String(defaultValue))

  const commit = (raw: string) => {
    const parsed = Number(raw)
    const next = Number.isFinite(parsed)
      ? Math.min(max, Math.max(min, parsed))
      : value
    setValue(next)
    setDraft(String(next))
  }

  const palette = {
    "--vibeui-slider-008-fill": `${((value - min) / (max - min)) * 100}%`,
    ...(accent ? { "--vibeui-slider-008-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-slider-008" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="slider-008"
        className={className}
        style={palette}
      >
        <label data-part="label" htmlFor={`${id}-range`}>
          {label}
        </label>
        <div data-part="row">
          <span data-part="rail">
            <input
              data-part="range"
              id={`${id}-range`}
              type="range"
              min={min}
              max={max}
              step={step}
              value={value}
              onChange={(event) => {
                setValue(Number(event.target.value))
                setDraft(event.target.value)
              }}
            />
          </span>
          <span data-part="field">
            <input
              data-part="number"
              id={`${id}-number`}
              type="number"
              inputMode="numeric"
              min={min}
              max={max}
              step={step}
              value={draft}
              aria-label={`${label}, точное значение`}
              onChange={(event) => setDraft(event.target.value)}
              onBlur={(event) => commit(event.target.value)}
              onKeyDown={(event) => {
                if (event.key === "Enter") {
                  commit(event.currentTarget.value)
                }
              }}
            />
            <span data-part="unit">{unit}</span>
          </span>
        </div>
        <p data-part="hint">
          от {min} до {max} {unit}, шаг {step}
        </p>
      </div>
    </>
  )
}