@lattice-ui/react-sliderFeature limitedimport Sliderdepends on runtime, focus, motionSlider 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
import { React } from "@lattice-ui/react-runtime";import { Slider } from "@lattice-ui/react-slider";import { Text, useTheme } from "@lattice-ui/react-style";
function SliderRow(props: { label: string; layoutOrder: number; value: number; onChange: (next: number) => void }) { const { theme } = useTheme();
return ( <frame BackgroundTransparency={1} LayoutOrder={props.layoutOrder} Size={UDim2.fromOffset(280, 44)}> <Text BackgroundTransparency={1} Font={Enum.Font.GothamMedium} Size={UDim2.fromOffset(200, 18)} Text={props.label} TextColor3={theme.colors.textPrimary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(230, 0)} Size={UDim2.fromOffset(50, 18)} Text={`${math.floor(props.value)}%`} TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Right} /> <Slider.Root max={100} min={0} onValueChange={props.onChange} step={1} value={props.value}> <Slider.Track asChild> <frame BackgroundColor3={theme.colors.surface} BorderSizePixel={0} Position={UDim2.fromOffset(0, 30)} Size={UDim2.fromOffset(280, 6)} > <uicorner CornerRadius={new UDim(0, theme.radius.full)} /> <Slider.Range asChild> <frame BackgroundColor3={theme.colors.accent} BorderSizePixel={0}> <uicorner CornerRadius={new UDim(0, theme.radius.full)} /> </frame> </Slider.Range> <Slider.Thumb asChild> <textbutton AutoButtonColor={false} BackgroundColor3={theme.colors.accentContrast} BorderSizePixel={0} Size={UDim2.fromOffset(16, 16)} Text="" > <uicorner CornerRadius={new UDim(0, theme.radius.full)} /> <uistroke Color={theme.colors.border} Thickness={1} /> </textbutton> </Slider.Thumb> </frame> </Slider.Track> </Slider.Root> </frame> );}
function SliderExample() { const { theme } = useTheme(); const [volume, setVolume] = React.useState(60); const [music, setMusic] = React.useState(35);
return ( <frame BackgroundColor3={theme.colors.surfaceElevated} BorderSizePixel={0} Size={UDim2.fromScale(1, 1)}> <uicorner CornerRadius={new UDim(0, theme.radius.lg)} /> <uistroke Color={theme.colors.border} Thickness={1} /> <uipadding PaddingBottom={new UDim(0, theme.space[16])} PaddingLeft={new UDim(0, theme.space[20])} PaddingRight={new UDim(0, theme.space[20])} PaddingTop={new UDim(0, theme.space[16])} /> <uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, theme.space[12])} />
<frame BackgroundTransparency={1} LayoutOrder={0} Size={UDim2.fromOffset(280, 40)}> <Text BackgroundTransparency={1} Font={Enum.Font.GothamBold} Size={UDim2.fromOffset(280, 18)} Text="Sound" TextColor3={theme.colors.textPrimary} TextSize={theme.typography.bodyMd.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(0, 22)} Size={UDim2.fromOffset(280, 16)} Text="Applies to this device only." TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> </frame>
<SliderRow label="Master volume" layoutOrder={1} onChange={setVolume} value={volume} /> <SliderRow label="Music" layoutOrder={2} onChange={setMusic} value={music} /> </frame> );}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.Root> <Slider.Track> <Slider.Range /> <Slider.Thumb /> </Slider.Track></Slider.Root>| Part | Required | Responsibility |
|---|---|---|
Slider.Root | yes | Owns clamped/stepped value state, drag lifecycle, and orientation, shared through context. |
Slider.Track | yes | The draggable rail; an input on it starts a drag toward that position. |
Slider.Range | no | The filled portion from the start of the track to the current value. |
Slider.Thumb | yes | The draggable handle positioned at the current value; also handles keyboard adjustment. |
Slider is single-thumb only: value/defaultValue are a single number, not an array. Compose two independent sliders if you need a min/max range.
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.
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.
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.
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.
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.
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.
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.
The track and thumb are Active and Selectable only while the slider is enabled. When disabled, input is ignored and selection removed. Keyboard adjustment fires through the thumb’s InputBegan, so the thumb must be able to receive selection for arrow and page keys to reach it.
Arrow, page and Home/End presses call onValueCommit as well as onValueChange — one commit per keypress, unlike a drag’s single commit on release. Debounce it if commit triggers network traffic.
Slider.Root renders no GuiObject, so you cannot size or position “the slider” through it. Size the track (or your asChild track element) instead, and wrap the slider in your own frame when it needs padding or a background.
API reference
Slider.Root
| Prop | Type | Description |
|---|---|---|
| value | number | Controlled value. Pair with onValueChange. |
| defaultValue | number | Initial value for uncontrolled usage. Defaults to min. |
| onValueChange | (value: number) => void | Called continuously as the value changes during drag or keyboard input. |
| onValueCommit | (value: number) => void | Called once when an interaction ends (drag release or keyboard commit). |
| min | number | Lower bound of the range. Defaults to 0. |
| max | number | Upper bound of the range. Defaults to 100. |
| step | number | Increment 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". |
| disabled | boolean | Disables drag and keyboard input and removes the track/thumb from selection. Defaults to false. |
| children | React.ReactNode | The slider parts. Root renders no instance of its own. |
Slider.Track
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge track behavior (drag start, ref, selection) onto the single child element instead of rendering the default frame. |
| children | React.ReactElement | The element to render. Required when asChild is set; otherwise rendered inside the default track (typically the Range and Thumb). |
Slider.Range
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge the animated fill onto the single child element; the child is stretched to fill the animated range frame, overriding its Position and Size. |
| children | React.ReactElement | The element to render. Required when asChild is set. |
Slider.Thumb
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge thumb behavior (drag start, keyboard handling, ref, selection, centered AnchorPoint) onto the single child element instead of the textbutton the part renders. |
| children | React.ReactElement | The element to render. Required when asChild is set. |