npx facet-rbxts add progressCopies ui/progress.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants,
@lattice-ui/react-runtime@^0.8.0 and @lattice-ui/react-progress@^0.8.0.
import { Progress } from "../shared/ui/progress";
<Progress value={72} /><Progress indeterminate />import React from "@rbxts/react";import { Label } from "../ui/label";import { Progress } from "../ui/progress";import { MODE } from "../facet-mode";
/** * Two determinate bars and the indeterminate sweep. The indicator's width is * Lattice's — this file only says the track is `bg-secondary` and the fill is * `bg-primary`. */export function Progresses() { return ( <frame className="flex-col gap-3 w-full h-fit"> <Label Text="Downloading map" /> <Progress value={30} /> <Label Text="Loading assets" /> <Progress value={72} /> <Label Text="Joining server" /> <Progress indeterminate /> </frame> );}import { fv } from "@facet-ui/react-variants";import { Progress as ProgressPrimitive } from "@lattice-ui/react-progress";import { getPassthroughProps, type PassthroughProps, React } from "@lattice-ui/react-runtime";import { type ClassName, cn } from "~/lib/utils";
/** * Lattice's `Progress.Root` renders no instance — it only turns `value`/`max` * into a ratio — so the track is drawn here. The indicator's width *is* that * ratio: motion owns its `Size`, animates it as the value moves, and sweeps it * back and forth when `indeterminate`. This file only says what both look like. */export const progressVariants = { // The track is `bg-primary/20`, not `bg-secondary`: shadcn's is the accent // colour at a fifth opacity, so the bar and its groove are the same hue and a // retheme moves both. `bg-secondary` was a different role that happened to // look similar in the zinc ramp. root: fv("h-2 w-full rounded-full bg-primary/20 overflow-hidden"), // `rounded-full` here is not in shadcn's, which relies on the track's // `overflow-hidden` to round the fill's leading edge. Roblox clips to a // rectangle, so the corner has to be on the indicator itself. indicator: fv("rounded-full bg-primary"),};
export type ProgressProps = { value?: number; max?: number; /** Sweep the indicator instead of mapping `value` — for work with no known end. */ indeterminate?: boolean; className?: ClassName;} & PassthroughProps<Frame>;
const OWN_PROPS = ["value", "max", "indeterminate", "className"] as const;
const NEUTRAL_PROPS = { BackgroundTransparency: 1, BorderSizePixel: 0,};
export function Progress(props: ProgressProps) { return ( <ProgressPrimitive.Root indeterminate={props.indeterminate} max={props.max} value={props.value}> <frame className={cn(progressVariants.root({ className: props.className }))} {...NEUTRAL_PROPS} {...getPassthroughProps<Frame>(props, OWN_PROPS)} > <ProgressPrimitive.Indicator className={cn(progressVariants.indicator())} /> </frame> </ProgressPrimitive.Root> );}Renders a Frame. Unknown props forward onto it and are type-checked against it, so a prop Frame does not accept is a compile error. The primitive owns Size on the indicator, which it animates from the value, so values you pass for it are ignored.
Props
| Prop | Type | Description |
|---|---|---|
| value | number | How far along. Read against max. |
| max | number | The end. Defaults to 100. |
| indeterminate | boolean | Sweep the indicator back and forth instead of mapping value — for work with no known end. |
| className | ClassName | Threaded into the root 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. |
There is no Text, no label and no percentage readout. A progress bar in this registry is two
rectangles; the words above it are a Label you place yourself.
The primitive draws nothing either
<ProgressPrimitive.Root indeterminate={props.indeterminate} max={props.max} value={props.value}> <frame className={cn(progressVariants.root({ className: props.className }))} …> <ProgressPrimitive.Indicator className={cn(progressVariants.indicator())} /> </frame></ProgressPrimitive.Root>Progress.Root renders no instance — it turns value and max into a ratio and nothing else — so
the track is a plain <frame> in the copied file, with the primitive wrapped around it. Same
inversion as Avatar.
The indicator has no width
export const progressVariants = { root: fv("h-2 w-full rounded-full bg-secondary overflow-hidden"), indicator: fv("rounded-full bg-primary"),};Two classes on the indicator, and neither is a size. Its Size is the value: Lattice’s motion
owns the property, animates it as the number moves, and sweeps it end to end when indeterminate.
overflow-hidden on the root is what keeps the fill’s square end inside the rounded track:
ClipsDescendants clips to the rectangle, and the indicator’s own rounded-full does the corners.
indeterminate is not a value
<Progress indeterminate /> ignores value entirely. Use it for work whose end you cannot measure
— a server round trip, an asset load with no progress signal — rather than passing a fake number
that never advances.