

<Preview name="PriceRangeSliderExample" />

## Overview [#overview]

The `Slider` component provides an accessible way for users to select a value or range of values along a continuous or discrete scale. Built on Base UI primitives, it offers full keyboard navigation, touch support, and optional tooltip displays.

## Usage [#usage]

```tsx
import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";
```

```tsx
<Slider defaultValue={[50]} max={100} step={1}>
  <SliderThumb />
</Slider>
```

<Accordions type="single">
  <Accordion title="URL State Management">
    Using Cubitt's URL-state hooks, you can sync the slider value with the URL by providing a `paramName`. The URL only updates when you release the slider thumb, not while dragging, to avoid performance issues:

    ```tsx
    // Slider value synced with ?volume=50
    <Slider paramName="volume" defaultValue={[50]} max={100} step={5}>
      <SliderThumb />
    </Slider>

    // Range slider synced with URL:
    <Slider
      paramName="price-range"
      defaultValue={[100, 500]}
      min={0}
      max={1000}
      step={10}
    >
      <SliderThumb />
      <SliderThumb />
    </Slider>

    // Advanced options:
    <Slider
      paramName="brightness"
      paramClearOnDefault={true}    // Remove param when value equals default
      onUrlValueChange={(value) => console.log('Brightness:', value)}
      defaultValue={[75]}
      max={100}
    >
      <SliderThumb />
    </Slider>
    ```
  </Accordion>
</Accordions>

## Examples [#examples]

### Default [#default]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="BasicSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-xs">
          <Slider defaultValue={[50]} max={100} step={1}>
            <SliderThumb />
          </Slider>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Range Slider [#range-slider]

Use two thumbs to select a range of values.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="RangeSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-xs">
          <Slider defaultValue={[100, 450]} min={0} max={600} step={1}>
            <SliderThumb />
            <SliderThumb />
          </Slider>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### With Labels [#with-labels]

Add labels to indicate the range or meaning of values.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SliderWithLabelsExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-xs">
          <div className="flex flex-col gap-3">
            <Label>Volume</Label>
            <Slider defaultValue={[50]} step={10}>
              <SliderThumb />
            </Slider>
            <span
              className="text-muted-foreground flex w-full items-center justify-between gap-2 text-xs font-medium"
              aria-hidden="true"
            >
              <span>Low</span>
              <span>High</span>
            </span>
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### With Tooltip [#with-tooltip]

Show the current value in a tooltip while dragging.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SliderWithTooltipExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, Label } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-xs">
          <div className="flex flex-col gap-3">
            <Label>Brightness</Label>
            <Slider
              defaultValue={[50]}
              step={10}
              showTooltip
              aria-label="Slider with tooltip"
            />
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### With Ticks [#with-ticks]

Display tick marks to indicate discrete values or steps.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SliderWithTicksExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";
    import { cn } from "@tilt-legal/cubitt-components/utilities";

    export default function Component() {
      const max = 12;
      const skipInterval = 2;
      const ticks = [...Array(max + 1)].map((_, i) => i);

      return (
        <div className="w-full max-w-xs">
          <div className="flex flex-col gap-3">
            <Label>Slider with ticks</Label>
            <div>
              <Slider defaultValue={[5]} max={max} aria-label="Slider with ticks">
                <SliderThumb />
              </Slider>
              <span
                className="text-muted-foreground mt-3 flex w-full items-center justify-between gap-1.125 px-0.5 text-xs font-medium"
                aria-hidden="true"
              >
                {ticks.map((_, i) => (
                  <span
                    key={i}
                    className="flex w-0 flex-col items-center justify-center gap-2"
                  >
                    <span
                      className={cn(
                        "bg-muted-foreground/70 h-1 w-px",
                        i % skipInterval !== 0 && "h-0.5"
                      )}
                    />
                    <span className={cn(i % skipInterval !== 0 && "opacity-0")}>
                      {i}
                    </span>
                  </span>
                ))}
              </span>
            </div>
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Price Range with Inputs [#price-range-with-inputs]

Combine sliders with number inputs for precise control.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="PriceRangeSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { SliderThumb, Label, Input } from "@tilt-legal/cubitt-components/primitives";
    import { useState, useId } from "react";
    import { Slider } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      const id = useId();
      const [sliderValue, setSliderValue] = useState([200, 800]);
      const [minInput, setMinInput] = useState("200");
      const [maxInput, setMaxInput] = useState("800");

      const minValue = 80;
      const maxValue = 900;

      const handleSliderChange = (value: number | readonly number[]) => {
        const values = Array.isArray(value) ? value : [value];
        setSliderValue(values);
        setMinInput(values[0].toString());
        setMaxInput(values[1].toString());
      };

      const validateMinInput = () => {
        const numValue = parseFloat(minInput);
        if (
          !isNaN(numValue) &&
          numValue >= minValue &&
          numValue <= sliderValue[1]
        ) {
          setSliderValue([numValue, sliderValue[1]]);
        } else {
          setMinInput(sliderValue[0].toString());
        }
      };

      const validateMaxInput = () => {
        const numValue = parseFloat(maxInput);
        if (
          !isNaN(numValue) &&
          numValue <= maxValue &&
          numValue >= sliderValue[0]
        ) {
          setSliderValue([sliderValue[0], numValue]);
        } else {
          setMaxInput(sliderValue[1].toString());
        }
      };

      return (
        <div className="space-y-4 w-full max-w-sm">
          <div className="flex flex-col gap-2.5">
            <Label>Price Range</Label>
            <Slider
              value={sliderValue}
              onValueChange={handleSliderChange}
              min={minValue}
              max={maxValue}
              step={10}
            >
              <SliderThumb />
              <SliderThumb />
            </Slider>
          </div>

          <div className="flex items-center justify-between gap-4">
            <div className="space-y-2.5">
              <Label htmlFor={`${id}-min`}>Min Price</Label>
              <Input
                id={`${id}-min`}
                type="number"
                value={minInput}
                onChange={(e) => setMinInput(e.target.value)}
                onBlur={validateMinInput}
              />
            </div>
            <div className="space-y-2.5">
              <Label htmlFor={`${id}-max`}>Max Price</Label>
              <Input
                id={`${id}-max`}
                type="number"
                value={maxInput}
                onChange={(e) => setMaxInput(e.target.value)}
                onBlur={validateMaxInput}
              />
            </div>
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Rating Slider [#rating-slider]

Create custom rating interfaces with emoji indicators.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="RatingSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import { SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";
    import { useState } from "react";
    import { Slider } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      const [value, setValue] = useState([3]);
      const labels = ["Awful", "Poor", "Okay", "Good", "Amazing"];

      return (
        <div className="space-y-3 w-full max-w-xs">
          <div className="flex items-center justify-between gap-2">
            <Label className="leading-6">Rate your experience</Label>
            <span className="text-sm font-medium">{labels[value[0] - 1]}</span>
          </div>
          <div className="flex items-center gap-2">
            <span className="text-2xl">😡</span>
            <Slider
              value={value}
              onValueChange={(newValue) =>
                setValue(Array.isArray(newValue) ? newValue : [newValue])
              }
              min={1}
              max={5}
            >
              <SliderThumb />
            </Slider>
            <span className="text-2xl">😍</span>
          </div>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Climate Control [#climate-control]

Complex example with multiple sliders and reset functionality.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="ClimateControlExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    "use client";

    import {
      SliderThumb,
      Label,
      Input,
      Button,
    } from "@tilt-legal/cubitt-components/primitives";
    import { useState, useRef } from "react";
    import { Slider } from "@tilt-legal/cubitt-components/primitives";
    import {
      Temperature,
      Droplet,
      Wind,
      ArrowRotateClockwise,
      } from "@tilt-legal/cubitt-icons/ui/outline";

    function ClimateSlider({
      minValue,
      maxValue,
      initialValue,
      defaultValue,
      label,
      unit,
      icon,
      onRegisterReset,
      }: {
      minValue: number;
      maxValue: number;
      initialValue: number[];
      defaultValue: number[];
      label: string;
      unit: string;
      icon: React.ReactNode;
      onRegisterReset: (resetFn: () => void) => void;
    }) {
      const [sliderValue,
      setSliderValue] = useState(initialValue);
      const [inputValue,
      setInputValue] = useState(initialValue[0].toString());

      // Register reset function
      React.useEffect(() => {
        onRegisterReset(() => {
          setSliderValue(defaultValue);
          setInputValue(defaultValue[0].toString());
        });
      },
      [onRegisterReset,
      defaultValue]);

      // Handle changes...

      return (
        <div className="w-full space-y-0.5">
          <div className="flex items-center gap-2">
            <div className="text-muted-foreground">{icon}</div>
            <Label className="text-sm font-medium">{label}</Label>
          </div>
          <div className="flex items-center gap-5">
            <Slider
              className="grow"
              value={sliderValue}
              onValueChange={(value) => {
                const values = Array.isArray(value) ? value : [value];
                setSliderValue(values);
                setInputValue(values[0].toString());
              }}
              min={minValue}
              max={maxValue}
              step={0.5}
            >
              <SliderThumb />
            </Slider>
            <Input
              className="h-8 w-16 px-2 py-1 text-center"
              type="number"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
            />
          </div>
        </div>
      );
    }

    export default function Component() {
      const resetFunctionsRef = useRef<(() => void)[]>([]);

      return (
        <div className="space-y-4 w-full max-w-sm">
          <div className="space-y-3">
            <ClimateSlider
              minValue={16}
              maxValue={30}
              initialValue={[22]}
              defaultValue={[22]}
              label="Temperature"
              unit="°C"
              icon={<Temperature className="w-4 h-4" />}
              onRegisterReset={(fn) => (resetFunctionsRef.current[0] = fn)}
            />
            {/* More sliders... */}
          </div>
          <Button
            className="w-full"
            variant="secondary"
            onClick={() => resetFunctionsRef.current.forEach((fn) => fn())}
          >
            <ArrowRotateClockwise size={16} />
            Reset to Defaults
          </Button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Vertical Slider [#vertical-slider]

Sliders can be oriented vertically for space-constrained layouts.

<Tabs
  items="['Preview',
'Code']"
>
  <Tab value="Preview">
    <Preview name="VerticalSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="flex h-40 justify-center">
          <Slider
            defaultValue={[2, 7]}
            max={10}
            orientation="vertical"
            aria-label="Vertical slider"
            showTooltip
            tooltipContent={(value) => `Value: ${value}`}
            tooltipVariant="light"
          />
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Disabled [#disabled]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="DisabledSliderExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-xs flex flex-col gap-4">
          <Slider defaultValue={[50]} disabled>
            <SliderThumb />
          </Slider>
          <Slider defaultValue={[25, 75]} disabled>
            <SliderThumb />
            <SliderThumb />
          </Slider>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### URL State [#url-state]

Sync the slider with the URL using `paramName`.

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="SliderURLStateExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    import { Slider, Label } from "@tilt-legal/cubitt-components/primitives";

    export default function Component() {
      return (
        <div className="w-full max-w-sm flex flex-col gap-3">
          <Label>Volume</Label>
          <Slider
            paramName="demo-volume"
            defaultValue={[50]}
            max={100}
            step={5}
            showTooltip
          />
          <span
            className="text-muted-foreground flex w-full items-center justify-between gap-2 text-xs font-medium"
            aria-hidden="true"
          >
            <span>0%</span>
            <span>100%</span>
          </span>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

## API Reference [#api-reference]

### Slider [#slider]

The root slider component with track and range indicator.

| Prop             | Type                                  | Default        | Description                             |
| ---------------- | ------------------------------------- | -------------- | --------------------------------------- |
| `defaultValue`   | `number[]`                            | —              | The default value when uncontrolled.    |
| `value`          | `number[]`                            | —              | The controlled value of the slider.     |
| `onValueChange`  | `(value: number \| number[]) => void` | —              | Callback fired when the value changes.  |
| `min`            | `number`                              | `0`            | The minimum value of the slider.        |
| `max`            | `number`                              | `100`          | The maximum value of the slider.        |
| `step`           | `number`                              | `1`            | The stepping interval.                  |
| `orientation`    | `"horizontal"` \| `"vertical"`        | `"horizontal"` | The orientation of the slider.          |
| `disabled`       | `boolean`                             | `false`        | Whether the slider is disabled.         |
| `showTooltip`    | `boolean`                             | `false`        | Show tooltip with value while dragging. |
| `tooltipContent` | `(value: number) => React.ReactNode`  | —              | Custom content for the tooltip.         |
| `tooltipVariant` | `"default"` \| `"light"`              | `"default"`    | The visual variant of the tooltip.      |
| `name`           | `string`                              | —              | The name for form submission.           |
| `className`      | `string`                              | —              | Additional CSS classes for the slider.  |

#### URL State Props [#url-state-props]

When provided with a `paramName`, the slider will sync its value with URL parameters via TanStack Router search params. The URL updates only when you release the thumb, not while dragging.

| Prop                  | Type                        | Default | Description                                                                  |
| --------------------- | --------------------------- | ------- | ---------------------------------------------------------------------------- |
| `paramName`           | `string`                    | —       | URL parameter name for syncing state with URL. Enables URL state management. |
| `onUrlValueChange`    | `(value: number[]) => void` | —       | Callback when URL parameter value changes.                                   |
| `paramClearOnDefault` | `boolean`                   | `true`  | Remove URL param when value equals default.                                  |
| `paramThrottle`       | `number`                    | —       | Throttle URL updates in milliseconds.                                        |
| `paramDebounce`       | `number`                    | —       | Debounce URL updates in milliseconds.                                        |

### SliderThumb [#sliderthumb]

The draggable thumb control for the slider.

| Prop        | Type     | Description                           |
| ----------- | -------- | ------------------------------------- |
| `className` | `string` | Additional CSS classes for the thumb. |
