Tessina UI
Tessina UI
GitHubIntroduction
InstallationUsageTheming
Components
ButtonIconButtonLinkSpinnerBadgeAvatarStatusChipShortcutSkeletonSurfaceProgressMeterRating
LabelFieldFieldsetCheckboxRadioSwitchSliderSelectComboboxSearchTextareaNumberFieldDate PickerTime PickerOTP InputFile UploadCalendar
AlertBannerToastTooltip
TabsAccordionCollapsibleBreadcrumbPaginationStepperSegmentedControlButtonGroupToggleButtonToggleGroupToolbarNavigation MenuMenubarBottom NavSidebar
Split ButtonFABDropdown MenuContextMenuCommandPopoverHoverCard
ContainerStackFlexGridAspectRatioSpacerCardCarouselDividerScroll Area
Table
ChatBubblePromptInputCodeBlock
ModalAlertDialogDrawerAction SheetTop Header MobileTop Header DesktopEmptyStateForm
Contributing
ComponentsLayout Blocks

Scroll Area

A scrollable container with custom overlay scrollbars that do not steal layout space. Supports vertical, horizontal, and both-axis content with a neutral branded thumb, RTL, and a compound API for full control.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  ScrollArea,
  ScrollAreaViewport,
  ScrollAreaScrollbar,
  ScrollAreaThumb,
  ScrollAreaCorner,
  ScrollAreaContent,
} from "@tessinaui/ui";
<ScrollArea className="h-72 w-64 rounded-md border border-border bg-card">
  <div className="p-4">{/* tall content */}</div>
</ScrollArea>

Showcase

When to use ScrollArea vs. native overflow

Use ScrollArea when…Use native overflow-auto when…
You want a thin, branded scrollbar that doesn't push contentThe OS scrollbar is fine
The container has a fixed height and tall contentScroll is at the page level
You need a consistent rounded thumb with size + visibility controlsYou don't need custom visuals
You want horizontal scrolling with a pretty railThe default rail is good enough

ScrollArea renders its scrollbars as overlays — they appear on top of the content, so the viewport width/height doesn't shift when they show/hide.

Anatomy

Most use-cases only need <ScrollArea>. When orientation is set, the appropriate scrollbar(s) and corner are auto-rendered.

<ScrollArea orientation="both">
  {/* content that overflows both axes */}
</ScrollArea>

For custom layouts, pass asChildParts and compose the primitives yourself:

<ScrollArea asChildParts>
  <ScrollAreaViewport>{/* content */}</ScrollAreaViewport>
  <ScrollAreaScrollbar orientation="vertical">
    <ScrollAreaThumb />
  </ScrollAreaScrollbar>
  <ScrollAreaScrollbar orientation="horizontal">
    <ScrollAreaThumb />
  </ScrollAreaScrollbar>
  <ScrollAreaCorner />
</ScrollArea>

Size

SizeTrack thicknessThumb
"sm"6pxThin — good for dense UIs, chat transcripts
"md" (default)10pxDefault — lists, panels
"lg"14pxProminent — long-form reading, data tables

Rounded

Applies to the thumb (the track is transparent).

ValueCSS
"none"rounded-none
"sm"rounded-sm
"md"rounded-md
"full" (default)rounded-full

Thumb colour

The thumb is a single neutral palette in both light and dark modes — built from the muted-foreground token. It composites at 50% opacity at rest, bumps to 70% on hover, and goes fully opaque while actively dragged.

Rationale: a scrollbar is chrome, not content. We deliberately don't ship semantic (error/warning/success/info) colouring for the thumb — use the surrounding surface, alert, or status components to signal intent instead.

Visibility

Controls when the scrollbar is rendered.

ValueBehaviour
"always" (default)Visible whenever the viewport overflows. Good for lists, logs, and most panels where users need the affordance.
"hover"Visible on hover or while scrolling; hidden otherwise. Use for clean canvases where the thumb would distract.
"scroll"Visible only while scrolling.
<ScrollArea visibility="hover">…</ScrollArea>

Rail

Controls whether the scrollbar has a visible track background under the thumb.

ValueBehaviour
false (default)Only the thumb renders; it sits as an overlay over the content. Minimal, calm, and matches most product surfaces.
trueA subtle muted background paints under the thumb to form a visible track — closer to a native OS scrollbar. Use when the thumb alone feels floating or when users benefit from seeing the full scrollable extent.
{/* Overlay — default */}
<ScrollArea>…</ScrollArea>

{/* With a visible rail under the thumb */}
<ScrollArea rail>…</ScrollArea>

Orientation

orientation controls which scrollbars render. The default is vertical-only.

{/* Row of cards that scrolls horizontally */}
<ScrollArea orientation="horizontal">
  <div className="flex gap-4 p-4 w-max">…</div>
</ScrollArea>

{/* Data table that scrolls in both dimensions */}
<ScrollArea orientation="both">
  <table className="w-max">…</table>
</ScrollArea>

When orientation="both", a transparent corner is rendered at the scrollbar intersection so the two rails don't overlap.

RTL

Pass dir="rtl" to the root. The vertical scrollbar flips to the logical end (visual left in RTL), and horizontal scrolling reverses direction.

<ScrollArea dir="rtl">
  <div className="text-right">…</div>
</ScrollArea>

Compound API

For advanced layouts — custom viewport wrappers, multiple content blocks, extra styling on specific parts — pass asChildParts and compose directly:

<ScrollArea asChildParts className="h-64 rounded-md border">
  <ScrollAreaViewport className="p-4">
    <h4>Section A</h4>
    <ul>…</ul>
    <h4>Section B</h4>
    <ul>…</ul>
  </ScrollAreaViewport>
  <ScrollAreaScrollbar orientation="vertical">
    <ScrollAreaThumb />
  </ScrollAreaScrollbar>
</ScrollArea>

ScrollAreaContent is also available if you want an inner sizing wrapper that still inherits the root's corner radius.

Accessibility

  • The viewport is the focusable element — it automatically receives tabindex={0} via Base UI when scrollable, so keyboard users can page through content with ArrowUp/ArrowDown/PageUp/PageDown.
  • The custom scrollbar is purely visual; keyboard scrolling is delegated to the browser's native behaviour on the viewport.
  • data-[hovering], data-[scrolling], and data-[orientation] attributes are surfaced by Base UI for CSS theming.
  • Because the scrollbars are overlays, they do not steal layout space — which means text reflow is consistent across devices where native scrollbars differ.

API Reference

ScrollArea (root)

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Scrollbar thickness
rounded"none" | "sm" | "md" | "full""full"Thumb corner radius
visibility"always" | "hover" | "scroll""always"When the scrollbar is visible
railbooleanfalsePaint a subtle muted background under the scrollbar to form a visible track/rail. When false, only the thumb is rendered as an overlay.
orientation"vertical" | "horizontal" | "both""vertical"Which scrollbars to render
dir"ltr" | "rtl""ltr"Reading direction
asChildPartsbooleanfalseOpt out of auto-rendering; compose parts manually
overflowEdgeThresholdnumber | { xStart, xEnd, yStart, yEnd }0Pixel threshold before overflow edge attributes apply

All other standard <div> props (including className, style, id, role, data-attributes) are forwarded to the root element.

ScrollAreaViewport

The scrollable container. Renders a <div>; props extend the Base UI primitive. Use className to add padding or constrain the inner content.

ScrollAreaScrollbar

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Which axis this scrollbar controls
keepMountedbooleanfalseKeep in DOM even when no overflow
size, visibility, rail—inheritedOverride the root's variant

ScrollAreaThumb

PropTypeDefaultDescription
rounded—inheritedOverride the root's variant

ScrollAreaCorner

Renders a <div> at the intersection of the horizontal and vertical scrollbars. Defaults to transparent.

ScrollAreaContent

Optional inner wrapper inside the viewport. Inherits border-radius via rounded-[inherit]. Useful when you want Base UI's content-size awareness for scrollbar math.

Notes

  • Built on @base-ui/react/scroll-area — the same headless primitive the design system uses for custom scroll affordances.
  • The scrollbar uses Base UI's data-[hovering] and data-[scrolling] attributes for visibility transitions, so styles automatically respond to mouse and scroll activity.
  • Overlay behaviour means the scrollbar never changes the viewport's width — layout stays stable on first paint and when the bar appears/disappears.
  • For platforms where native scrollbars are always visible (Windows classic), this component gives you a consistent cross-platform look.

Divider

A thin line that separates content. Horizontal or vertical, solid / dashed / dotted, five thickness sizes, tonal colours, optional inline label, and RTL support.

Table

A complete primitive set for tabular data — header, body, footer, sortable columns, sticky header, row states, plus cell helpers for avatars, status, currency amounts, and actions. Designed in the Wise / PrimeReact tradition.

On this page

PlaygroundInstallationUsageShowcaseWhen to use ScrollArea vs. native overflowAnatomySizeRoundedThumb colourVisibilityRailOrientationRTLCompound APIAccessibilityAPI ReferenceScrollArea (root)ScrollAreaViewportScrollAreaScrollbarScrollAreaThumbScrollAreaCornerScrollAreaContentNotes