Facetcomponents

Scroll area

A viewport with a drawn scrollbar — and the component whose height has to come from above it.

Terminal window
npx facet-rbxts add scroll-area

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

import { ScrollArea } from "../shared/ui/scroll-area";
<frame className="flex-col w-full h-32 rounded-md border border-border">
<ScrollArea>
<frame className="flex-col w-full h-fit p-3 gap-2">…</frame>
</ScrollArea>
</frame>
Seven rows in a 128px box. The height and the border are on the wrapper, not on the ScrollArea.

The parts

PartRendersClasses
ScrollAreaFramew-full h-full overflow-hidden
ScrollBarFramerounded-full, plus the edge insets for its orientation

ScrollArea renders a ScrollBar for you. The export exists for the case where you want a second one on the other axis.

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 height comes from the parent

So the size goes on a wrapper, and so does anything else the recipe states. What does cross from a call-site class is whatever the recipe leaves alone — rounded-md and border-border reach the instance fine, because the root recipe names neither a corner nor a stroke.

A scroll area that hugs its content has nothing to scroll, so something above it has to resolve a height either way. Putting it on a wrapper frame is the one arrangement that always works.

Props

ScrollArea

PropTypeDescription
type"auto" | "always" | "scroll"auto shows the bar while scrolling, always keeps it, scroll matches the platform. Read by the primitive.
scrollHideDelayMsnumberHow long the bar lingers after a scroll stops.
childrenReact.ReactNodeWhat scrolls. Give it h-fit so it can be taller than the viewport.
classNameClassNameThreaded into the root recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see above.

ScrollBar

PropTypeDescription
orientation"vertical" | "horizontal"Which edge it pins to, and which axis the thumb sizes on. Defaults to vertical.
classNameClassNameThreaded into the scrollbar recipe's className slot inside the component.

Two scrollbars, one of them invisible

viewport: fv("size-full scrollbar-none"),

The viewport is a Roblox ScrollingFrame, which draws a scrollbar of its own. scrollbar-none hides it, because the visible one is the ScrollBar beside it — a frame Lattice sizes and positions from the scroll ratio, and fades after scrollHideDelayMs.

Losing that class gives you both bars at once, which is the failure mode to recognise.

The root has no flex-*

The scrollbar is pinned to an edge with inset classes — right-0 top-0 h-full w-2 for a vertical one — and a UIListLayout would pull it into the flow and lay it out beside the viewport instead. Slider omits its layout for the same reason.