Latticecomponents

Slider

A single-thumb slider owning clamped, stepped value state and drag plus keyboard input.

@lattice-ui/react-sliderFeature limitedimport Sliderdepends on runtime, focus, motion

Slider is the primitive for picking a number from a continuous range: volume, sensitivity, brightness. It owns the value, clamping to [min, max] and snapping to step, and turns pointer drags and keyboard input into value changes.

Your component renders only the track, the filled range and the thumb.

Preview

Edit

Import

import { Slider } from "@lattice-ui/react-slider";

Anatomy

Root, Track and Thumb form the minimum useful slider. Range shows the filled portion up to the current value.

Slider anatomy

Slider anatomy
<Slider.Root>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
PartRequiredResponsibility
Slider.RootyesOwns clamped/stepped value state, drag lifecycle, and orientation, shared through context.
Slider.TrackyesThe draggable rail; an input on it starts a drag toward that position.
Slider.RangenoThe filled portion from the start of the track to the current value.
Slider.ThumbyesThe draggable handle positioned at the current value; also handles keyboard adjustment.

Examples

Basic volume slider

Uncontrolled state seeded with defaultValue. Every part renders unstyled, so the track, range and thumb each carry their own size and colour. The primitive owns only the geometry it computes from the value. For “save on release”, use onValueCommit.

VolumeSlider.tsx
import { Slider } from "@lattice-ui/react-slider";
export function VolumeSlider(props: { onSave: (volume: number) => void }) {
return (
<Slider.Root
defaultValue={40}
min={0}
max={100}
step={5}
onValueCommit={(volume) => props.onSave(volume)}
>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
);
}

Controlled slider with live readout

Pass value and onValueChange when something outside needs the number as it moves. Slider.Root renders no instance of its own, so the track participates directly in the surrounding row layout.

BrightnessSlider.tsx
import { useState } from "@rbxts/react";
import { Slider } from "@lattice-ui/react-slider";
export function BrightnessSlider() {
const [brightness, setBrightness] = useState(70);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(330, 32)}>
<uilistlayout
FillDirection={Enum.FillDirection.Horizontal}
Padding={new UDim(0, 12)}
VerticalAlignment={Enum.VerticalAlignment.Center}
/>
<Slider.Root value={brightness} onValueChange={setBrightness}>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(48, 20)}
Text={`${brightness}%`}
TextColor3={Color3.fromRGB(236, 241, 249)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</frame>
);
}

Fine-grained steps with min and max

Every value is clamped to [min, max] and snapped to the nearest multiple of step counted from min. A slider from 0.1 to 2 in 0.05 increments lands only on 0.10, 0.15, 0.20. Format the readout yourself.

SensitivitySlider.tsx
import { useState } from "@rbxts/react";
import { Slider } from "@lattice-ui/react-slider";
export function SensitivitySlider(props: { onCommit: (sensitivity: number) => void }) {
const [sensitivity, setSensitivity] = useState(1);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(330, 32)}>
<uilistlayout
FillDirection={Enum.FillDirection.Horizontal}
Padding={new UDim(0, 12)}
VerticalAlignment={Enum.VerticalAlignment.Center}
/>
<Slider.Root
value={sensitivity}
onValueChange={setSensitivity}
onValueCommit={props.onCommit}
min={0.1}
max={2}
step={0.05}
>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(48, 20)}
Text={string.format("%.2f", sensitivity)}
TextColor3={Color3.fromRGB(236, 241, 249)}
TextXAlignment={Enum.TextXAlignment.Left}
/>
</frame>
);
}

Vertical orientation

Set orientation="vertical" for a column-style control. The default track becomes 10x220, and the range fills from the bottom. Dragging maps the pointer’s Y position to the value, with the top of the track as max.

AmbienceChannel.tsx
import { useState } from "@rbxts/react";
import { Slider } from "@lattice-ui/react-slider";
export function AmbienceChannel() {
const [level, setLevel] = useState(60);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(64, 260)}>
<uilistlayout
HorizontalAlignment={Enum.HorizontalAlignment.Center}
Padding={new UDim(0, 8)}
SortOrder={Enum.SortOrder.LayoutOrder}
/>
<Slider.Root orientation="vertical" value={level} onValueChange={setLevel}>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
<textlabel
BackgroundTransparency={1}
Size={UDim2.fromOffset(48, 20)}
Text={`${level}`}
TextColor3={Color3.fromRGB(236, 241, 249)}
/>
</frame>
);
}

Custom visuals with asChild

Every part accepts asChild. The track slot carries the drag-start handler, ref and selection props. The thumb slot also forces an AnchorPoint of (0.5, 0.5). Slider.Range stretches your child to fill the animated fill frame, so style it with colour and corners rather than sizing it.

StyledSlider.tsx
import { useState } from "@rbxts/react";
import { Slider } from "@lattice-ui/react-slider";
export function StyledSlider() {
const [value, setValue] = useState(50);
return (
<Slider.Root value={value} onValueChange={setValue}>
<Slider.Track asChild>
<frame
BackgroundColor3={Color3.fromRGB(30, 33, 42)}
BorderSizePixel={0}
Size={UDim2.fromOffset(280, 8)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<Slider.Range asChild>
<frame BackgroundColor3={Color3.fromRGB(255, 255, 255)} BorderSizePixel={0}>
<uicorner CornerRadius={new UDim(1, 0)} />
<uigradient
Color={
new ColorSequence(Color3.fromRGB(88, 142, 255), Color3.fromRGB(140, 190, 255))
}
/>
</frame>
</Slider.Range>
<Slider.Thumb asChild>
<textbutton
AutoButtonColor={false}
BackgroundColor3={Color3.fromRGB(240, 244, 252)}
BorderSizePixel={0}
Size={UDim2.fromOffset(20, 20)}
Text=""
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uistroke Color={Color3.fromRGB(88, 142, 255)} Thickness={2} />
</textbutton>
</Slider.Thumb>
</frame>
</Slider.Track>
</Slider.Root>
);
}

Keyboard adjustment

With the thumb focused, Right/Up add step, Left/Down subtract it, PageUp/PageDown move by step * 10, and Home/End jump to the bounds. Each both changes and commits, so keep onValueCommit cheap. Return/Space commits without changing the value.

FieldOfViewSlider.tsx
import { useState } from "@rbxts/react";
import { Slider } from "@lattice-ui/react-slider";
export function FieldOfViewSlider(props: { onCommit: (fov: number) => void }) {
const [fov, setFov] = useState(80);
return (
<Slider.Root
value={fov}
onValueChange={setFov}
onValueCommit={props.onCommit}
min={60}
max={110}
step={1}
>
<Slider.Track>
<Slider.Range />
<Slider.Thumb />
</Slider.Track>
</Slider.Root>
);
}

How it behaves

Value, clamping, and stepping

The value is a single number, controlled via value/onValueChange or uncontrolled via defaultValue, which defaults to min. min and max default to 0 and 100, normalized so the lower bound is the smaller. step defaults to 1, and a zero or negative step falls back to 1.

Change vs. commit

onValueChange fires as the value moves — every drag tick and value-changing keypress. onValueCommit fires at the end of an interaction: drag release, each keyboard adjustment, or Return/Space. Change is the live-preview channel, commit the persist channel.

Rendering and layout

Slider.Root renders no instance, so Slider.Track is the outermost GuiObject. The default track is a frame sized 260x10 or 10x220. The range is an animated fill frame, and the thumb a 16x16 textbutton anchored at its centre. All three are replaceable with asChild.

Dragging

A press on Track or Thumb starts a drag and jumps the value to the pressed position. While dragging, the root follows UserInputService input changes even after the pointer leaves the track, then commits on release. Touch drags are tracked per input object.

Keyboard adjustment

Slider.Thumb handles keyboard input when focused, and each press both changes and commits. Return/Space commits without changing. These are keyboard key codes — gamepad buttons are not mapped to value changes.

Orientation

orientation is "horizontal", the default, or "vertical". It governs which pointer axis maps to the value, how the default track sizes itself, and how the range and thumb position themselves: from the left horizontally, from the bottom vertically.

Composition with asChild

Slider.Track and Slider.Thumb merge their behaviour onto your single child through Slot. The slot’s Active, Selectable, ref and InputBegan handling win, and the thumb forces AnchorPoint to (0.5, 0.5). Slider.Range with asChild keeps the animated fill frame and stretches your child to fill it, overriding its Position and Size. Express its look through colour, gradients, corners and strokes.

Motion

Slider.Range and Slider.Thumb animate toward their target with a response recipe, tightening while a drag is in progress so the handle stays under the pointer.

API reference

Slider.Root

PropTypeDescription
valuenumberControlled value. Pair with onValueChange.
defaultValuenumberInitial value for uncontrolled usage. Defaults to min.
onValueChange(value: number) => voidCalled continuously as the value changes during drag or keyboard input.
onValueCommit(value: number) => voidCalled once when an interaction ends (drag release or keyboard commit).
minnumberLower bound of the range. Defaults to 0.
maxnumberUpper bound of the range. Defaults to 100.
stepnumberIncrement the value snaps to. Defaults to 1; non-positive values fall back to 1.
orientation"horizontal" | "vertical"Axis the slider runs along. Defaults to "horizontal".
disabledbooleanDisables drag and keyboard input and removes the track/thumb from selection. Defaults to false.
childrenReact.ReactNodeThe slider parts. Root renders no instance of its own.

Slider.Track

PropTypeDescription
asChildbooleanMerge track behavior (drag start, ref, selection) onto the single child element instead of rendering the default frame.
childrenReact.ReactElementThe element to render. Required when asChild is set; otherwise rendered inside the default track (typically the Range and Thumb).

Slider.Range

PropTypeDescription
asChildbooleanMerge the animated fill onto the single child element; the child is stretched to fill the animated range frame, overriding its Position and Size.
childrenReact.ReactElementThe element to render. Required when asChild is set.

Slider.Thumb

PropTypeDescription
asChildbooleanMerge thumb behavior (drag start, keyboard handling, ref, selection, centered AnchorPoint) onto the single child element instead of the textbutton the part renders.
childrenReact.ReactElementThe element to render. Required when asChild is set.