ScrollView

Updated: Sep 24, 2026
Use ScrollView when arbitrary content can overflow a region. The toolkit owns the scroll viewport, boundary fades, floating single-axis scrollbar, keyboard scrolling, and focus reveal, which keeps focused content clear of each fade. The application provides the content and sizes the region.
For a collection of list rows, prefer VerticalList. It combines ScrollView with the standard row container for consistent list layout.

Usage

import { ScrollView, TextStyle, TextView } from '@wearables-ui-toolkit/mrbd';

type ArticleBodyProps = {
  paragraphs: string[];
};

export function ArticleBody({ paragraphs }: ArticleBodyProps) {
  return (
    <ScrollView ariaLabel="Article">
      <div className="article-body">
        {paragraphs.map((text, index) => (
          <TextView as="p" key={index} textStyle={TextStyle.BODY2}>
            {text}
          </TextView>
        ))}
      </div>
    </ScrollView>
  );
}
Prefer to lay out content in an application-owned wrapper inside the scroll view. This keeps viewport behavior under the toolkit’s control while the application owns spacing and content layout:
.article-body {
  display: flex;
  flex-direction: column;
  gap: var(--uit-spacing-small);
  padding-block: var(--uit-spacing-medium);
}
For horizontal content, set orientation. The component then switches both the scroll axis and the active pair of fades:
import {
  Card,
  ScrollView,
  ScrollViewOrientation,
} from '@wearables-ui-toolkit/mrbd';

type CaptureStripProps = {
  captures: ReadonlyArray<{ id: string; label: string }>;
};

export function CaptureStrip({ captures }: CaptureStripProps) {
  return (
    <ScrollView
      ariaLabel="Recent captures"
      orientation={ScrollViewOrientation.HORIZONTAL}
      scrollbarEnabled={false}
    >
      <div className="capture-row">
        {captures.map(capture => (
          <Card key={capture.id}>{capture.label}</Card>
        ))}
      </div>
    </ScrollView>
  );
}
A horizontal scroll view arranges its own content element as a row. An application-owned wrapper inside it remains a plain block, so give that wrapper the row layout and spacing it needs:
.capture-row {
  display: flex;
  gap: var(--uit-spacing-small);
  padding-inline: var(--uit-spacing-medium);
}

Props

The full prop type is ScrollViewProps. This is a closed prop set: ScrollView does not spread additional DOM attributes onto the frame or viewport, so callers can pass only the props below.
PropTypeDefaultDescription
children
ReactNode
—
Scrollable content.
orientation
ScrollViewOrientation
ScrollViewOrientation.VERTICAL
Active scroll axis.
fadingEdgeEnabled
boolean
true
Draws fades at the boundaries of the active axis. Turning it off removes all fades and their focus-reveal insets.
topFadingEdgeLength
number
Standard fade length
Top fade length in pixels. Applies only to vertical orientation.
bottomFadingEdgeLength
number
Standard fade length
Bottom fade length in pixels. Applies only to vertical orientation.
leftFadingEdgeLength
number
Standard fade length
Left fade length in pixels. Applies only to horizontal orientation.
rightFadingEdgeLength
number
Standard fade length
Right fade length in pixels. Applies only to horizontal orientation.
insetForHeader
boolean
false
Reserves top padding so content begins below an overlapping header.
headerHeight
number
Standard header height
Reserved inset height in pixels. Set it to the overlapping header’s height. Used only when insetForHeader is set.
scrollbarEnabled
boolean
true
Controls whether the single-axis scrollbar can appear.
showScrollbar
boolean
—
Alias for scrollbarEnabled. scrollbarEnabled takes precedence when both are set; the scrollbar is on when neither is set.
width
number or string
'100%'
Scroll frame width.
height
number or string
'100%'
Scroll frame height.
className
string
''
Class applied to the outer frame.
scrollContainerClassName
string
''
Class applied to the scrolling viewport.
style
CSSProperties
—
Inline styles applied to the outer frame. They merge after width and height, so a width or height here takes precedence.
ariaLabel
string
—
Accessible name for the scroll region.
tabIndex
number
-1
Tab index of the scrolling viewport.
onScroll
(scrollTop: number) => void
—
Runs on every scroll event with the viewport’s vertical scroll offset.
The component forwards its ref to the scrolling viewport element. Callers can use that element’s scrollTop, scrollLeft, and scrollTo APIs directly.

Types and enums

ScrollViewOrientation
  • VERTICAL
  • HORIZONTAL

Behavior

Structure. The toolkit renders an outer frame with width, height, className, and style. Inside it, the scrolling viewport receives scrollContainerClassName, tabIndex, and the accessible name. A content element wraps the caller’s children and defines the region’s focus boundary. Fades and the scrollbar are viewport siblings, so they neither affect layout nor scroll with the content.
One axis at a time.orientation selects the only scrollable axis and the fade props the toolkit reads. For example, topFadingEdgeLength has no effect in a horizontal scroll view. In horizontal orientation, the content element becomes a top-aligned row sized to its content, so children follow the scroll axis. In vertical orientation, it is at least as tall as the viewport.
Fading edges. Each active edge strengthens in proportion to its distance from the content boundary and reaches full strength after one fade length. The fade is invisible while content is flush with that boundary. Each gradient runs from black at the boundary to transparent inward. On the additive-light display, an unlit pixel emits no light, so the fade removes light from the content beneath it instead of tinting that content. The toolkit does not add spacer content for fades.
Scrollbar. The toolkit aligns the scrollbar with the active axis on a short floating track centered at the region’s trailing edge: right for vertical content and bottom for horizontal content. It renders only when content overflows, appears while scrolling, and hides after a short idle delay. The toolkit sizes its handle from the viewport-to-content ratio down to a minimum and offsets it with the scroll position. The scrollbar is decorative and hidden from assistive technology.
Keyboard. When the viewport itself has focus, arrow keys on the active axis scroll by one step, PageUp and PageDown scroll by one viewport, and Home and End move to the boundaries. If an arrow key points past an already reached boundary, ScrollView asks the surrounding directional navigation system to move focus out of the region instead of consuming the key. PageUp, PageDown, Home, and End always remain with the region. Every keyboard scroll explicitly requests smooth behavior, which keeps motion consistent even when the host page sets a different scroll-behavior.
Scroll to focused content. When a descendant receives focus, ScrollView reveals it inside the active fade insets so a fade does not dim the focused content. If both insets cannot fit in the viewport, the toolkit shrinks them proportionally rather than consuming the region. For the first or last focusable element, the container settles at the true scroll boundary when that alignment keeps the element fully visible and clear of the active fade and header inset. If leading or trailing non-focusable content pushes the focused element outside that safe area, normal focus reveal takes precedence and outward arrow presses scroll the remaining content incrementally. The toolkit checks again after container animations settle so expansion does not move the focused element beneath an overlay. When the viewport cannot fit the element and all preferred fade insets, the toolkit contracts fade clearance while preserving header clearance as far as space allows. Avoid permanent padding intended only to move a boundary past a fade; the focus-reveal calculation already owns that clearance.
Focus that cannot move. If a focused descendant has no focusable neighbor in the arrow direction but the region can still scroll that way, ScrollView consumes the key and scrolls by up to half a viewport or the remaining distance, whichever is smaller. If a neighbor exists in that direction, the component leaves the key available while any part of the current focus remains within the physical viewport. Focus can then move to a partially visible neighbor, and normal focus reveal brings it clear of the fade. While the focused element is entirely outside the viewport, arrow presses continue scrolling instead of moving focus so the element can return to view first. The region’s own first and last focusable elements are the exception: when settling at the true boundary still keeps the element fully visible and clear of the active fade and header inset, the region snaps to that boundary and passes the key on. This lets focus leave a fully scrolled region instead of trapping it.
Nested scrollers. When focus belongs to an overflowing inner scroll region, the outer ScrollView leaves reveal behavior to the inner region. This is a safety mechanism, not a supported composition. Use exactly one scroll container per axis. In particular, do not wrap a vertical ScrollView around a VerticalList, because both components would own vertical scrolling and focus reveal.
Header inset. With insetForHeader, the toolkit gives the scrolling viewport top padding equal to headerHeight plus the content-origin offset from an enclosing Page, when present. Content then begins below an overlapping header while respecting the page’s content origin. Because the inset is viewport padding, fades and the scrollbar remain aligned with the frame.
Sizing. Width and height fill the parent by default, so the application-owned parent determines the available region. ScrollView remeasures its box when the box or its children change, keeping fades and the scrollbar accurate as content grows or shrinks. Prefer to size the parent from the available viewport rather than a fixed device size, which keeps the layout responsive.

Accessibility

  • Provide ariaLabel so the scrolling region has an accessible name; ScrollView does not infer one from its content.
  • tabIndex defaults to -1. The region stays outside the tab sequence but can receive programmatic and directional-navigation focus. Set it to 0 only when the region itself needs to be a tab stop, typically when non-focusable content must remain keyboard-scrollable.
  • The toolkit marks fades and the scrollbar aria-hidden, so assistive technology does not announce these decorative elements.
  • Let the scroll view reach both horizontal screen edges, then inset the application-owned content with a spacing token.
  • Give the region an ariaLabel that identifies what scrolls.
  • Make the region a tab stop when it contains plain text and no focusable descendants, so keyboard users can scroll it.
  • Consider hiding the scrollbar for short horizontal strips where it adds visual noise.

Avoid these patterns

  • Avoid same-axis nested scroll containers. Each axis needs one owner for predictable focus and directional navigation.
  • Avoid permanent padding that only clears a fade; focus reveal already accounts for active fade insets.
  • Treat onScroll as a vertical offset only. It always reports scrollTop; read the forwarded ref’s scrollLeft for a horizontal region.
  • Avoid generated class names for the internal viewport or content elements. Lay out an application-owned wrapper inside children, or target the viewport with scrollContainerClassName.