# Slider

> Three instances whose geometry is entirely the primitive's — and the one component whose track deliberately has no layout.

Source: https://docs.astra-void.xyz/facet/components/slider/

```bash
npx facet-rbxts add slider
```

Copies `ui/slider.tsx`, plus `lib/utils.ts`. Needs `@facet-ui/react-variants`,
`@lattice-ui/react-runtime@^0.8.0` and `@lattice-ui/react-slider@^0.8.0`.

```tsx
import { Slider } from "../shared/ui/slider";

<Slider defaultValue={70} onValueCommit={(value) => saveVolume(value)} />
```

_Interactive preview: A plain slider, one stepped by 10, and a disabled one. The thumb overhangs the track because it is size-4 on an h-2 bar._

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `value` | `number` | Controlled value. Pass it with onValueChange. |
| `defaultValue` | `number` | Uncontrolled starting value. |
| `onValueChange` | `(value: number) => void` | Fires continuously as the thumb moves. |
| `onValueCommit` | `(value: number) => void` | Fires once per gesture, when the drag or the keypress lets go. This is the one to save from. |
| `min` | `number` | Lower bound. |
| `max` | `number` | Upper bound. |
| `step` | `number` | Quantises the value. The thumb still moves smoothly; the value does not. |
| `orientation` | `"horizontal" \| "vertical"` | Which axis the thumb travels along. |
| `disabled` | `boolean` | Blocks the drag, and fades the track and the thumb with opacity-50. |
| `className` | `ClassName` | Threaded into the track recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site. |

This is the one component in the registry with **no passthrough bag**. There is no
`PassthroughProps<Frame>` on `SliderProps` and no `getPassthroughProps` in the body: three separate
instances render here and there is no single one an unknown prop should land on.

## The track has no `flex-*`, and that is the point

```tsx
export const sliderVariants = {
  track: fv("h-2 w-full rounded-full bg-secondary"),
  range: fv("rounded-full bg-primary"),
  thumb: fv("size-4 rounded-full border border-primary bg-background"),
};
```

Every other container in the registry opens with `flex-row` or `flex-col`. This one does not, and
removing that class is the component's whole design decision.

A `flex-*` class lowers to a `UIListLayout`, and a `UIListLayout` **positions every child it has**.
The range's fill and the thumb's travel are both `Position`/`Size` on instances inside the track,
written by Lattice as the value moves. Add a layout and the list overwrites them on the next frame.

> **The thumb rides the track, not a slot inside it**
>
> `size-4` on an `h-2` bar means the thumb is twice the track's height and hangs over both edges —
> which is what a slider knob looks like, and only possible because nothing is laying it out. The
> primitive centres its anchor; the overhang follows.
>
> What Studio still has to confirm is that the overhang lands symmetrically and the range fill stops
> exactly under the thumb. Loom renders it, but the
> [preview is not Studio](https://docs.astra-void.xyz/facet/getting-started/scope-and-status.md#what-is-not-covered).

## `onValueChange` versus `onValueCommit`

`onValueChange` fires on every frame of a drag. `onValueCommit` fires once, when the gesture ends.

For a volume slider you want both: the first to hear the change live, the second to write it to a
`DataStore`. Wiring a save to `onValueChange` means one request per frame of a drag.

## Disabled fades in two places

```tsx
<SliderPrimitive.Track className={sliderVariants.track({ className: cn(disabled && "opacity-50", props.className) })}>
  <SliderPrimitive.Range … />
  <SliderPrimitive.Thumb className={cn(sliderVariants.thumb(), disabled && "opacity-50")} />
</SliderPrimitive.Track>
```

The track and the thumb each state the fade, because
[`opacity-*` does not cross a component boundary](https://docs.astra-void.xyz/facet/guides/component-conventions.md#3-nothing-inherits) —
the thumb is a primitive child, not JSX the compiler can see under the track. The registry's rule is
the same one it always is: nothing inherits, so every instance states its own appearance.
