Lattice components

Scroll Area

Scroll-container primitive that wraps a Roblox ScrollingFrame, tracks its canvas metrics, and drives custom scrollbars and thumbs with overflow-aware visibility.

@lattice-ui/scroll-area Stable direction import ScrollArea depends on core

Scroll Area is the primitive for building a scroll container with custom-styled scrollbars on top of Roblox’s native ScrollingFrame. Viewport is the scrolling surface; Root reads its canvas metrics and decides when scrollbars should show; and Scrollbar/Thumb render a draggable indicator wired to the viewport’s CanvasPosition. Corner fills the gap where both axes overlap.

Reach for Scroll Area when you want the native scroll feel of a ScrollingFrame — wheel, touch drag, momentum — but with scrollbars you style yourself and overflow-aware visibility that hides them when there is nothing to scroll.

Import

import { ScrollArea } from "@lattice-ui/scroll-area";

Anatomy

Root provides the metrics context. Viewport holds your content. Add a Scrollbar (with a Thumb inside) per axis, and a Corner when you show both.

ScrollArea anatomy
<ScrollArea.Root>
<ScrollArea.Viewport>{/* content */}</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical">
<ScrollArea.Thumb orientation="vertical" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner />
</ScrollArea.Root>
PartRequiredResponsibility
ScrollArea.RootyesHolds the viewport ref, tracks per-axis metrics, and computes scrollbar visibility from overflow and type.
ScrollArea.ViewportyesThe ScrollingFrame that scrolls content and reports its canvas/window sizes back to Root.
ScrollArea.ScrollbarnoA track for one axis; a press on empty track jumps the canvas toward that position.
ScrollArea.ThumbnoThe draggable handle inside a scrollbar; sized and positioned to the scroll ratio.
ScrollArea.CornernoFills the intersection square, shown only when both scrollbars are visible.

Example

A vertically scrolling list with a custom-styled scrollbar and thumb.

PatchNotes.tsx
import { ScrollArea } from "@lattice-ui/scroll-area";
export function PatchNotes(props: { lines: string[] }) {
return (
<ScrollArea.Root type="scroll">
<frame BackgroundColor3={Color3.fromRGB(18, 20, 26)} Size={UDim2.fromOffset(280, 200)}>
<ScrollArea.Viewport asChild>
<scrollingframe BackgroundTransparency={1} Size={UDim2.fromScale(1, 1)}>
<uilistlayout Padding={new UDim(0, 6)} SortOrder={Enum.SortOrder.LayoutOrder} />
{props.lines.map((line, index) => (
<textlabel
key={index}
AutomaticSize={Enum.AutomaticSize.Y}
BackgroundTransparency={1}
Size={UDim2.new(1, 0, 0, 0)}
Text={line}
TextColor3={Color3.fromRGB(224, 230, 240)}
TextWrapped
/>
))}
</scrollingframe>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="vertical">
<frame BackgroundColor3={Color3.fromRGB(40, 46, 60)} Size={UDim2.new(0, 6, 1, 0)}>
<ScrollArea.Thumb orientation="vertical">
<frame BackgroundColor3={Color3.fromRGB(120, 132, 156)}>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
</ScrollArea.Thumb>
</frame>
</ScrollArea.Scrollbar>
</frame>
</ScrollArea.Root>
);
}

How it behaves

Scrolling and metrics

ScrollArea.Viewport renders a ScrollingFrame with AutomaticCanvasSize on both axes and the native scrollbars hidden (ScrollBarThickness = 0). It registers the frame with Root and listens to CanvasPosition, AbsoluteCanvasSize, and AbsoluteWindowSize changes, pushing the per-axis viewportSize, contentSize, and scrollPosition into context on every change. Because the underlying control is a real ScrollingFrame, mouse-wheel, touch-drag, and gamepad scrolling all work natively — the primitive layers custom scrollbars on top of that. Use asChild to supply your own scrollingframe if you need to set its size or styling.

Scrollbar visibility

Root derives overflow per axis (content larger than the viewport) and resolves visibility from type:

  • "auto" (default) — show a scrollbar only when that axis overflows.
  • "always" — show whenever the axis overflows; never auto-hide.
  • "scroll" — show on scroll activity, then auto-hide after scrollHideDelayMs (default 600ms) of inactivity, only while overflowing.

Scrollbar and Corner read these flags and toggle their own Visible. Corner shows only when both the vertical and horizontal scrollbars are visible.

Thumb sizing and dragging

ScrollArea.Thumb computes its size and offset from the axis metrics: its scale along the axis is the viewport-to-content ratio, and its position tracks the current scroll. Pressing the thumb starts a drag tracked through UserInputService (mouse or touch); pointer movement maps to a new CanvasPosition, clamped to the scroll range. Pressing the empty Scrollbar track (outside the thumb) jumps the canvas toward the pressed position. All scroll changes flow through Root.setScrollPosition, which clamps to [0, contentSize - viewportSize] and writes the viewport’s CanvasPosition.

Orientation

Scrollbar and Thumb each require an orientation of "vertical" or "horizontal". The default rendered scrollbar pins to the right edge (vertical) or bottom edge (horizontal); the thumb sizes itself along that axis. Provide one scrollbar/thumb pair per axis you want to expose, and add a Corner when both are present.

API reference

ScrollArea.Root

Prop Type Description
type "auto" | "always" | "scroll" Scrollbar visibility strategy. Defaults to "auto".
scrollHideDelayMs number For type="scroll", how long after activity before scrollbars auto-hide. Defaults to 600; floored at 0.
children React.ReactNode Viewport, scrollbars, thumbs, and corner.

ScrollArea.Viewport

Prop Type Description
asChild boolean Render your own scrollingframe instead of the default; the scroll props and metrics ref are projected onto it.
children React.ReactElement With asChild, the scrollingframe element; otherwise the content placed inside the default viewport.

ScrollArea.Scrollbar

Prop Type Description
orientation "vertical" | "horizontal" Which axis this scrollbar controls. Required.
asChild boolean Render your own track element via Slot; visibility and track-press handling are projected onto it.
children React.ReactElement With asChild, the track element; otherwise content placed inside the default track, typically a Thumb.

ScrollArea.Thumb

Prop Type Description
orientation "vertical" | "horizontal" Which axis this thumb belongs to. Required; should match its scrollbar.
asChild boolean Render your own thumb element via Slot; size, position, and drag handling are projected onto it.
children React.ReactElement With asChild, the thumb element; otherwise content placed inside the default thumb.

ScrollArea.Corner

Prop Type Description
asChild boolean Render your own corner element via Slot; its visibility is bound to both scrollbars being shown.
children React.ReactElement With asChild, the corner element; otherwise content placed inside the default corner.