Facetcomponents

Textarea

The one recipe in the registry that declares no height — because the primitive grows it line by line.

Terminal window
npx facet-rbxts add textarea

Copies ui/textarea.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants, @lattice-ui/react-runtime@^0.8.0 and @lattice-ui/react-textarea@^0.8.0.

import {
Textarea,
TextareaDescription,
TextareaInput,
TextareaLabel,
} from "../shared/ui/textarea";
<Textarea maxRows={5} minRows={3} onValueCommit={submitReport}>
<TextareaLabel Text="What happened?" />
<TextareaInput />
<TextareaDescription Text="A moderator reads this — keep it to what you saw." />
</Textarea>
Three rows to start, five at most. Type into it and the box grows a line at a time.

The parts

PartRendersClasses
TextareaFrameflex-col gap-2 w-full h-fit
TextareaInputTextBoxw-full rounded-md border border-input px-3 py-2 text-left text-sm font-normal text-foreground placeholder-muted-foreground focus:border-ring
TextareaLabelTextButtonw-full h-fit text-left text-sm font-medium text-foreground
TextareaDescriptionTextLabelw-full h-fit whitespace-normal text-left text-xs font-normal text-muted-foreground

Four parts, not five — there is no TextareaMessage. A multi-line box that needs a validation line can reach for TextFieldMessage, or the file is yours to add one.

Renders a TextBox. Unknown props forward onto it and are type-checked against it, so a prop TextBox does not accept is a compile error. The primitive owns Size on the Y axis, which it grows with the text, so values you pass for it are ignored.

The input declares no height

input: fv(
"w-full rounded-md border border-input px-3 py-2 …",
),

Rule 1 of the conventions is declare both axes, always — a Roblox instance with an unresolved axis renders at zero and takes the row above it with it. This recipe breaks that rule on purpose, and it is the only one that does.

Textarea.Input owns Size.Y. It measures the wrapped text and sets the height between minRows and maxRows on every keystroke. A height class here would not lose an argument with the primitive — it would be overwritten a frame later, which is worse, because the first render would look right.

Props

Textarea

PropTypeDescription
valuestringControlled value. Pass it with onValueChange.
defaultValuestringUncontrolled starting text.
onValueChange(value: string) => voidFires on every keystroke.
onValueCommit(value: string) => voidFires when the box loses focus, with the text as it stands.
autoResizebooleanGrow with the text. On by default; the box then sizes between minRows and maxRows.
minRowsnumberFloor for the grown height.
maxRowsnumberCeiling. Past it the box scrolls instead.
disabledbooleanBlocks focus and typing.
readOnlybooleanFocusable, not editable.
invalidbooleanCarries the state into Lattice's context. The red border comes from the input's own invalid.
classNameClassNameThreaded 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.

TextareaInput

PropTypeDescription
invalidbooleanAdds border-destructive.
disabledbooleanAdds opacity-50.
classNameClassNameThreaded into the input recipe's className slot inside the component.

invalid is stated on the root and on the input, for the reason Text field spells out: nothing inherits, so the context half and the appearance half are two different props on two different instances.

autoResize off

With autoResize={false} the primitive stops touching Size.Y, and the recipe’s missing height becomes the problem rule 1 warns about: give the input one.

This is the rare case where a call-site className is the right place to do it. A class written there is resolved at that call site and arrives as instance properties, which the component’s own recipe then overwrites for the properties it also names — and this recipe names no height, so <TextareaInput className="h-24" /> survives. Anything the recipe does state, like w-full, does not. The mechanism, and why it reads backwards from cn’s own composition rules, is in overriding from the call site.