npx facet-rbxts add labelCopies ui/label.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants and
@lattice-ui/react-runtime.
import { Label } from "../shared/ui/label";
<Label Text="Display name" />A second text colour is a second element, not a className on this one — a class written here is
lowered before Label ever sees it, and then overwritten by the component’s own recipe. See
Overriding from the call site.
import React from "@rbxts/react";import { Label } from "../ui/label";import { MODE } from "../facet-mode";
export function Labels() { return ( <frame className="flex-col gap-2 w-full h-fit"> <Label Text="Display name" /> {/* A second colour is a second component, not a `className` on this one: a class written here is lowered at the call site and then overwritten by the component's own recipe. See the Vela note in the guide. */} <textlabel className="size-fit text-xs font-normal text-muted-foreground" BackgroundTransparency={1} Text="Shown to other players." /> </frame> );}import { fv, type VariantProps } from "@facet-ui/react-variants";import { getPassthroughProps, type PassthroughProps, React } from "@lattice-ui/react-runtime";import { cn } from "~/lib/utils";
// `text-foreground` is the one token shadcn does not write: its label inherits// the page colour, and nothing inherits here. The rest is shadcn's exactly.export const labelVariants = fv("text-foreground text-sm leading-none font-medium");
export type LabelProps = VariantProps<typeof labelVariants> & { Text?: string;} & PassthroughProps<TextLabel>;
const OWN_PROPS = ["className", "Text"] as const;
const NEUTRAL_PROPS = { BackgroundTransparency: 1, BorderSizePixel: 0,};
/** * A leaf, so it draws its own `Text` rather than delegating to `TextSlot` — * there is nothing to compose around. */export function Label(props: LabelProps) { const passthrough = getPassthroughProps<TextLabel>(props, OWN_PROPS);
return ( <textlabel className={cn(labelVariants({ className: props.className }))} Text={props.Text ?? ""} {...NEUTRAL_PROPS} AutomaticSize={Enum.AutomaticSize.XY} {...passthrough} /> );}Renders a TextLabel. Unknown props forward onto it and are type-checked against it, so a prop TextLabel does not accept is a compile error.
Props
| Prop | Type | Description |
|---|---|---|
| Text | string | The label. Drawn as this instance's own Text — unlike Button, there is nothing to compose around, so there is no TextSlot. |
| className | ClassName | Threaded into the 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. |
Everything else is forwarded onto the TextLabel.
The recipe
export const labelVariants = fv("text-foreground text-sm font-medium");That is the whole thing — no variant axes. fv() with no config is still worth using over a bare
string: it gives you the className slot with the right ordering, and it is the seam a variant axis
grows into if you add one.
font-medium is not optional. Vela leaves FontFace untouched when no font-* token appears, and
Roblox’s untouched default is LegacyArial — a different typeface at a visibly different size from
the SourceSansPro everything else resolves to. Every text recipe in the registry declares a weight
for that reason. See Text and labels.
A leaf, so it draws its own text
Button and Badge delegate to TextSlot because they compose — a label beside an icon, a child
element instead of a string. Label has nothing to compose around, so it draws Text directly:
<textlabel className={cn(labelVariants({ className: props.className }))} Text={props.Text ?? ""} {...NEUTRAL_PROPS} AutomaticSize={Enum.AutomaticSize.XY} {...passthrough}/>AutomaticSize is set as an instance prop rather than through a class, and it sits before the
passthrough spread so a consumer can override it. Without a resolved size, a label collapses the
automatic sizing of whatever contains it — see
Component conventions.