Scroll Area

Free

A styled scrollbar drawn over a genuinely scrolling element — never in place of one. Minimum grabbable thumb with the track arithmetic corrected for it, click-to-page on the track, and edge fades that are the only affordance a hidden scrollbar leaves.

</>sv-scroll-area scrollscrollbaroverflowlayout
import { SvScrollAreaComponent } from '@styloviz/scroll-area';

Demo & usage

Basic usage

Usage example — open the gallery for the full interactive demo.

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Always-visible thumb

example.htmlAngular
<sv-scroll-area variant="always" maxHeight="20rem">…</sv-scroll-area>

No thumb, fades only

example.htmlAngular
<sv-scroll-area variant="hidden" [edgeFade]="true" maxHeight="12rem">…</sv-scroll-area>

Manual measurement

Content is observed automatically. Turn observation off only when you drive measurement yourself — then call measure() after every change.

example.htmlAngular
<sv-scroll-area #area [observeContent]="false" maxHeight="16rem">…</sv-scroll-area>

// after loading more rows
area.measure();

Horizontal, RTL-safe

The thumb, the fades and track paging follow the inline start, so the same markup works under dir="rtl".

example.htmlAngular
<sv-scroll-area orientation="horizontal" ariaLabel="Recent files">
  <div class="flex gap-3">…cards…</div>
</sv-scroll-area>

When to use

  • A bounded region — a panel, a dropdown, a log — that needs a scrollbar matching the rest of the design.
  • You want the native scrollbar hidden without losing anything the native scrollbar gave you.
  • For scrolling the page itself, do not use this: style the document scrollbar or leave it alone.

Async states

None — this component has nothing to load, so it has no loading, empty or error state to drive.

Inputs

NameTypeDefaultDescription
variantScrollAreaVariant'auto'When the thumb is drawn.
maxHeightstring'16rem'Max height of the scrolling box, as a CSS length.
minThumbnumberSV_MIN_THUMB_PXSmallest grabbable thumb, in px.
edgeFadebooleantrueFade the edges that have content behind them. Once the native chrome is hidden, a list that scrolls looks exactly like a list that does not. This is the only affordance left.
ariaLabelstring''Accessible name for the scrolling region.
observeContentbooleantrueRe-measure automatically when the viewport or the content resizes, with no scroll event needed. Turn off only to drive `measure()` yourself — e.g. a virtualised list that already knows its size.
tabStopScrollAreaTabStop'auto'Whether the region is a tab stop. `'auto'` makes it one only while it can actually scroll — short content adds no empty tab stop.
orientationScrollAreaOrientation'vertical'Which axis the drawn thumb, the edge fades and track paging follow. `'horizontal'` normalises `scrollLeft` for RTL, so the thumb and the fades start at the inline start in either direction.

Outputs

EventPayload typeDescription
scrollednumberEmitted on every scroll, with the current position along the axis: `scrollTop` when vertical, the px from the INLINE START when horizontal (direction-normalised, so it is never negative in RTL).

Customization & theming

  • Three thumb policies: `auto` (on hover / scroll), `always`, `hidden`.
  • Any CSS length as `maxHeight`.
  • Tune the smallest grabbable thumb with `minThumb` (24px by default).
  • Edge fades on or off via `edgeFade`.
  • `orientation="horizontal"` draws the thumb, fades and track paging along the x axis, normalised for RTL.
  • `tabStop`: `auto` (a tab stop only while scrollable — the default), `always`, or `never` (for content that is itself focusable).
  • `observeContent` (default on) re-measures on content or viewport resize; turn it off to call `measure()` yourself.
  • Variant axes (X-8): `variant`, `minThumb` and `edgeFade` are appearance; `orientation` and `maxHeight` are layout; `observeContent` and `tabStop` are behavioural modes. All combine. Limits: one drawn axis at a time — in `horizontal` mode vertical overflow still scrolls natively but has no drawn thumb (both-axis is not built yet).

Best practices

  • Do: Give the region an `ariaLabel` so its purpose is announced when a screen reader enters it.
  • Tip: Measurement (X-4): automatic by default — the viewport and the content box are observed (`svElementSize`, one reading per frame), so content that grows or shrinks with no scroll event re-measures on its own, and the first measurement runs after the first render. Call `measure()` only when `observeContent` is off, where there is no `ResizeObserver` (and on the server), or after a change that moves no box (a text-direction flip, content escaping its wrapper by absolute positioning).
  • Tip: Scroll roots (X-9): the scrolling element is the inner `[data-sv-port]`, not the host — listen there (or use `scrolled`), and `scrollIntoView` inside it works natively. The thumb and fades are absolutely positioned against the component's own wrapper, so no ancestor positioning or containing block is involved, and nothing is portalled (not applicable). No breakpoints: size it with `maxHeight`. The tab stop (`tabStop="auto"`) counts overflow on BOTH axes — a wide table in a short vertical area still scrolls natively on x and stays reachable.
  • Tip: Focus (X-10): the region is a tab stop only while it can scroll (`tabStop="auto"`). If content shrinks while the region holds focus, it keeps its tab stop until focus leaves, so focus is never dropped. Scrolling never moves focus.
  • Tip: Platform limits (X-11): the native scrollbar is hidden with `scrollbar-width: none` + `::-webkit-scrollbar`, so OS "always show scrollbars" preferences are overridden (use `variant="always"` for those users). RTL `scrollLeft` sign conventions differ between engines (spec negative, legacy Chrome, legacy Edge); the component probes once and normalises. Content observation needs `ResizeObserver` (all evergreen browsers).
  • Tip: Forced colours (X-7/X-11): the translucent thumb would repaint as Canvas and vanish, so it opts out of adjustment and paints `CanvasText`; the port's box-shadow focus ring becomes a real inset outline. The edge fades are gradients, which forced colours drops — the thumb (`variant="always"`) is the scroll affordance there.
  • Tip: Keep `edgeFade` on: once the native chrome is hidden, a list that scrolls looks exactly like one that does not.
  • Don't: Don't rebuild this on a transform inside an `overflow: hidden` box — that is what loses the keyboard.

Accessibility

  • The element genuinely scrolls, so Page Up/Down, Home/End, arrow keys, spacebar, trackpad momentum, `scrollIntoView`, find-in-page auto-scroll and every screen reader's virtual cursor keep working.
  • `role="region"` with `tabindex="0"` while it can scroll, because a scrollable region must be reachable by keyboard — and no tab stop when everything fits, where it would be a stop that does nothing. `tabStop` overrides it.
  • In RTL the vertical thumb sits on the left like a native scrollbar, and a horizontal thumb and its track paging run from the inline start.
  • A visually-hidden read-out reports the scroll position and how much of the content is visible — the cue a hidden scrollbar removes, and one a screen reader user never had.
  • Clicking the track pages by one viewport, as every native scrollbar on every platform does, rather than jumping the thumb to the pointer.

Feedback on Scroll Area

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.