@lattice-ui/progress Stable direction
import Progress
depends on core , motion Progress is the primitive for visualizing how far along a task is: loading bars, download meters, XP bars, and quest trackers. Root clamps the value against a maximum and derives a 0..1 ratio; Indicator animates a fill to that ratio; and Spinner provides a self-rotating element for work that has no measurable progress.
Reach for Progress when you have a bounded value you want to render as a fill, or when you only need an indeterminate busy indicator. The primitive owns the math and the motion — your component owns the colors, sizing, and surrounding layout.
Import
import { Progress } from "@lattice-ui/progress";Anatomy
Root provides the value context that Indicator reads. Spinner is independent: it needs no Root and is used on its own for indeterminate states.
<Progress.Root> <Progress.Indicator /></Progress.Root>
<Progress.Spinner />| Part | Required | Responsibility |
|---|---|---|
Progress.Root | yes | Clamps the value to [0, max], derives the fill ratio, and shares it via context. |
Progress.Indicator | no | A clipped track whose inner fill animates its width to the current ratio. |
Progress.Spinner | no | A standalone, self-rotating element for indeterminate work. Does not use Root. |
Example
A determinate download bar driven by app state, with a custom-colored fill passed through asChild.
import { useState } from "@rbxts/react";import { Progress } from "@lattice-ui/progress";
export function DownloadBar() { const [percent, setPercent] = useState(0);
return ( <frame BackgroundColor3={Color3.fromRGB(24, 26, 32)} Size={UDim2.fromOffset(280, 16)} > <uicorner CornerRadius={new UDim(0, 8)} />
<Progress.Root value={percent} max={100}> <Progress.Indicator asChild> <frame BackgroundColor3={Color3.fromRGB(102, 200, 140)}> <uicorner CornerRadius={new UDim(0, 8)} /> </frame> </Progress.Indicator> </Progress.Root> </frame> );}For work with no measurable end, drop in a spinner:
import { Progress } from "@lattice-ui/progress";
export function Saving() { return <Progress.Spinner speedDegPerSecond={240} />;}How it behaves
Value and range
Progress.Root accepts a value and a max (default 100, floored to at least 1). The value is clamped to [0, max], and the fill ratio is clampedValue / max. Root is controllable: pass value to drive it from app state, or defaultValue (default 0) to run uncontrolled. onValueChange mirrors the core controllable-state contract.
Indeterminate
Set indeterminate on Root to signal work without a known endpoint. The shared ratio resolves to 0.25, and Progress.Indicator renders a fixed 0.35-width fill instead of tracking the value — a partial bar you can animate or style as a busy state. Use Spinner instead when you want a rotating glyph rather than a bar.
Motion
Progress.Indicator is a clipped (ClipsDescendants) track. Its inner fill animates its Size width toward the current ratio using the default progress response recipe from @lattice-ui/motion. Pass transition to override that recipe — for snappier or slower fills. By default the fill is a solid frame in Color3.fromRGB(102, 156, 255); use asChild to swap in your own fill element (its Position and Size are forced to fill the track).
Spinner rotation
Progress.Spinner rotates its root GuiObject every Heartbeat, advancing Rotation by speedDegPerSecond (default 180) times the frame delta. When spinning is false, the rotation loop stops and the element is hidden (Visible = false). The default render is a 22x22 ring with an accent dot; with asChild, your child element is rotated and its visibility is bound to spinning.
Progress.Root ignores out-of-range input by clamping rather than erroring, so a value above max simply renders a full bar. Drive value from your own state for live progress; use defaultValue only for a static initial fill that never changes.
Progress.Spinner does not read Root context — it is a self-contained indeterminate indicator. You can place it anywhere, including outside a Progress.Root. Rotation runs on RunService.Heartbeat, so it keeps animating as long as the element is mounted and spinning is true.
API reference
Progress.Root
| Prop | Type | Description |
|---|---|---|
| value | number | Controlled progress value. Clamped to [0, max]. |
| defaultValue | number | Initial value for uncontrolled usage. Defaults to 0. |
| onValueChange | (value: number) => void | Called when the controllable value changes. |
| max | number | Upper bound of the range. Defaults to 100; floored to a minimum of 1. |
| indeterminate | boolean | Marks the progress as having no known endpoint. Forces the shared ratio to a fixed value. Defaults to false. |
| children | React.ReactNode | The indicator and any surrounding content. |
Progress.Indicator
| Prop | Type | Description |
|---|---|---|
| transition | ResponseMotionConfig | Overrides the default progress response recipe used to animate the fill width. |
| asChild | boolean | Render your own fill element instead of the default frame. Its Position and Size are forced to fill the track. |
| children | React.ReactElement | The fill element to render. Required when asChild is set. |
Progress.Spinner
| Prop | Type | Description |
|---|---|---|
| spinning | boolean | Whether the spinner rotates and is visible. Defaults to true. |
| speedDegPerSecond | number | Rotation speed in degrees per second. Defaults to 180. |
| asChild | boolean | Rotate your own element instead of the default ring. Its visibility is bound to spinning. |
| children | React.ReactElement | The element to rotate. Required when asChild is set. |