Noctis

Scroll area

Custom thin scrollbars over native scrolling. The browser still does the scrolling; Base UI overlays slim, neutral bars that follow a visibility policy, widen and darken as you reach for the thumb, linger briefly after you stop, sit on the correct inline edge under RTL, and re-derive inside a recessed surface. The viewport stays the keyboard-focusable, named region — the bars are pure paint.

Vertical

Bound the ScrollArea.Root with a height (and width) so its content overflows, wrap the content in a ScrollArea.Content, and render a vertical ScrollArea.Scrollbar. A bare scrollbar renders its own ScrollArea.Thumb, so there's nothing else to wire. Hover the bar and the thumb firms up and the gutter widens.

Both axes

Render a scrollbar per axis for a region that overflows in both directions, plus a ScrollArea.Corner for the square where the two bars meet. Each bar carries its own orientation, and the horizontal bar mirrors to the correct edge under RTL.

Visibility

type on ScrollArea.Root sets the visibility policy, matching the macOS overlay convention. hover (default) fades the bar in on hover or while scrolling; scroll shows it only while scrolling; auto shows it whenever the content overflows; always keeps it drawn as a persistent channel (with a faint gutter track). scrollHideDelay (default 600) is the rest-delay before the bar fades back out — the fade-in stays immediate, so the bar never flickers.

type="hover"Shows on area hover or while scrolling.
type="scroll"Shows only while scrolling.
type="auto"Shows whenever the content overflows.
type="always"Stays drawn, with a gutter track.

Density

size scales the gutter — md by default, or a thinner sm for dense lists and data tables. A coarse (touch) pointer automatically fattens the bar and lengthens the thumb so it stays grabbable, regardless of size.

Edge fade

Set fade to mask the overflowing edges with a colourless scroll-shadow, signalling there's more content beyond the fold — the cue an auto-hiding overlay bar can't give while it's faded out. The mask reveals the background (no accent, no rounding), keys off the logical overflow edges so it mirrors under RTL, and is off by default so it never dims content inside a bordered surface.

Inside a recessed surface

Drop a scroll area inside a Surface with shade="sunken" (or any data-elevation="sunken" scope) and the thumb and track re-derive to that scope — the scrollbar reads as part of the well, not the canvas. Hover the thumb and it darkens against the recessed fill.

Accessibility

Native scrolling is preserved, so the overlay bar, thumb, and corner are decorative — they carry aria-hidden and never role="scrollbar" (that role is only for a fully custom JS scrollbar, and would duplicate the native scroll semantics). The viewport is the accessible region instead:

  • It carries a role — group by default (a quiet, labelled boundary), or region for a page-level area that should appear in the landmarks rotor.
  • It needs an accessible name: pass aria-label or aria-labelledby. A development build warns if neither is set, because a focusable region with no name announces nothing.
  • Base UI keeps it conditionally focusable — in the tab order (and showing the focus ring) only while it overflows, so a region that fits its content is never an empty, silent focus stop.

The region below holds only non-focusable prose, so the viewport itself is what Tab reaches — then the arrow, Page, Home, and End keys scroll it.

Keyboard

KeyAction
TabMove focus onto the viewport while it overflows and holds no focusable content, making the region reachable.
/ Scroll the focused viewport vertically by a line.
/ Scroll the focused viewport horizontally; the arrows follow the writing direction under RTL.
Page Up / Page DownScroll the focused viewport by a page.
Home / EndJump to the start or end of the scrollable content.
Space / Shift+SpacePage down / up.

Scrolling is the browser's own — the custom bars only restyle it, so every native key and the OS smooth-scroll behaviour are preserved.

Anatomy

Compose a scroll area from its parts. ScrollArea.Root bounds the region, sets the visibility policy, and clips the overlaid bars to its corners; it renders no scroll behaviour of its own — native scrolling stays on the viewport.

  • ScrollArea.Root — the bounding box that overlays the scrollbars and carries the policy. Props: type (hover | scroll | auto | always), scrollHideDelay, size (sm | md), overscroll (contain | auto), fade, and track.
  • ScrollArea.Viewport — the natively scrolled, accessible region; give it a bounded size so its content overflows, a role (group by default, or region), and an accessible name. Keyboard-focusable while it overflows, with a visible focus ring.
  • ScrollArea.Content — the intrinsic-width wrapper, so horizontal overflow is measured from the content's natural width.
  • ScrollArea.Scrollbar — one axis's decorative bar. Props: orientation (vertical | horizontal, default vertical) and keepMounted (keep the bar in the DOM even without overflow; defaulted on for type="always").
  • ScrollArea.Thumb — the draggable handle; a bare Scrollbar renders one for you.
  • ScrollArea.Corner — the square where a vertical and a horizontal bar meet.

Every rendered part carries a data-slot (noctis-scroll-area, noctis-scroll-area-viewport, noctis-scroll-area-content, noctis-scroll-area-scrollbar, noctis-scroll-area-thumb, noctis-scroll-area-corner) for host-side styling — pair it with the root's data-type/data-size/data-overscroll and the state attributes (data-orientation, data-hovering, data-scrolling, data-has-overflow-x, data-has-overflow-y, and the logical data-overflow-*-start/-end).

On surfaces

The same control re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well. It stays legible on every layer.

root
elevated
menu
sunken

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every scroll area in that region retunes — e.g. .dense { --noctis-scroll-area-scrollbar-inline-size: 0.375rem; } thins every bar beneath it. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that forward to Base UI's Scroll Area list just the props they pass through. Expand a row for the full type and description.

ScrollArea.Root

Prop

ScrollArea.Viewport

Prop

ScrollArea.Content

Prop

ScrollArea.Scrollbar

Prop

ScrollArea.Thumb

Prop

ScrollArea.Corner

Prop

AttributeDescription
data-slotThe rendered scroll-area element.
data-orientation`horizontal` | `vertical` — the orientation of a scrollbar or thumb.
data-type`hover` | `scroll` | `auto` | `always` — the visibility policy, stamped on the root.
data-size`sm` | `md` — the gutter scale, stamped on the root.
data-overscroll`contain` | `auto` — the scroll-chaining behaviour, stamped on the root.
data-fadePresent on the root when the edge-fade mask is enabled.
data-trackPresent on the root when the gutter track is painted (set by `track`, or by `type="always"`).
data-hoveringPresent on the scrollbar while the pointer is over the scroll area (the fade-in cue).
data-scrollingPresent on the scrollbar and viewport while the user is scrolling.
data-has-overflow-xPresent when the content is wider than the viewport (a horizontal scrollbar is warranted).
data-has-overflow-yPresent when the content is taller than the viewport (a vertical scrollbar is warranted).
data-overflow-x-startPresent on the root/viewport while content is hidden past the inline-start edge (drives the fade).
data-overflow-x-endPresent on the root/viewport while content is hidden past the inline-end edge (drives the fade).
data-overflow-y-startPresent on the root/viewport while content is hidden past the block-start edge (drives the fade).
data-overflow-y-endPresent on the root/viewport while content is hidden past the block-end edge (drives the fade).