Components

Slider Field

A labeled slider with live value readout, range (two-thumb) support, custom formatting, and min/max captions.

input

Installation

npx cano-ui add slider-field

Installs @radix-ui/react-slider automatically if missing.

Source

"use client"

import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"

import { cn } from "@/lib/utils"

export interface SliderFieldProps {
  label?: string
  min?: number
  max?: number
  step?: number
  /** One value for a single thumb, two for a range. */
  value?: number[]
  defaultValue?: number[]
  onValueChange?: (value: number[]) => void
  /** Formats the value readout, e.g. (v) => `$${v}`. */
  formatValue?: (value: number) => string
  /** Hides the live value readout next to the label. */
  showValue?: boolean
  /** Shows min/max captions under the track. */
  showRange?: boolean
  disabled?: boolean
  className?: string
}

export function SliderField({
  label,
  min = 0,
  max = 100,
  step = 1,
  value,
  defaultValue,
  onValueChange,
  formatValue = (v) => String(v),
  showValue = true,
  showRange = false,
  disabled,
  className,
}: SliderFieldProps) {
  const [internal, setInternal] = React.useState(
    value ?? defaultValue ?? [min]
  )
  const current = value ?? internal

  function handleChange(next: number[]) {
    setInternal(next)
    onValueChange?.(next)
  }

  return (
    <div
      data-slot="slider-field"
      className={cn("flex w-full flex-col gap-3", disabled && "opacity-50", className)}
    >
      {label || showValue ? (
        <div className="flex items-baseline justify-between gap-2">
          {label ? (
            <span className="text-sm font-medium text-foreground">{label}</span>
          ) : null}
          {showValue ? (
            <span className="text-sm tabular-nums text-muted-foreground">
              {current.map(formatValue).join(" – ")}
            </span>
          ) : null}
        </div>
      ) : null}
      <SliderPrimitive.Root
        min={min}
        max={max}
        step={step}
        value={value}
        defaultValue={value ? undefined : (defaultValue ?? [min])}
        onValueChange={handleChange}
        disabled={disabled}
        aria-label={label}
        className="relative flex w-full touch-none items-center select-none"
      >
        <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted">
          <SliderPrimitive.Range className="absolute h-full bg-primary" />
        </SliderPrimitive.Track>
        {current.map((_, i) => (
          <SliderPrimitive.Thumb
            key={i}
            className="block size-4 shrink-0 rounded-full border border-primary bg-background shadow-sm transition-shadow hover:ring-4 hover:ring-ring/20 focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:outline-none disabled:pointer-events-none"
          />
        ))}
      </SliderPrimitive.Root>
      {showRange ? (
        <div className="flex justify-between text-xs text-muted-foreground">
          <span>{formatValue(min)}</span>
          <span>{formatValue(max)}</span>
        </div>
      ) : null}
    </div>
  )
}

This exact file lands in your project at components/ui/slider-field.tsx. It is yours to edit.