Components / Structure
ScrollArea
A box that fills its parent and scrolls what does not fit. It can follow the end while content arrives and hold an overlay, such as a "3 new" pill, that stays put while the content scrolls.
When to useLink to "When to use"
- The scrolling middle of an app shell: a panel between a header and a status bar that scrolls instead of making the page longer.
- Logs, chats and activity streams:
followkeeps the latest in view,onAtBottomChangetells you when someone scrolled up to read, andoverlayholds the way back.
Use something else when:
- the content is a long list of rows people pick from: VirtualList (it renders only the rows in view, and has
followandoverlaytoo) - a box only needs to clip or scroll and has nothing else to do:
overflowon Stack, Inline or Grid - the content is code: CodeBlock scrolls its own
UsageLink to "Usage"
import { ScrollArea, type ScrollAreaHandle } from "@arshad-shah/cynosure";
const area = useRef<ScrollAreaHandle>(null);
const [atBottom, setAtBottom] = useState(true);
<Stack fill gap="0">
<Toolbar label="Activity">…</Toolbar>
<ScrollArea
label="Activity"
follow
handle={area}
onAtBottomChange={setAtBottom}
overlay={!atBottom && <Button onClick={() => area.current?.scrollToEnd()}>Latest</Button>}
>
{events}
</ScrollArea>
</Stack>The area is a flex share and 100% of its parent both ways, with a minimum size of 0: in a flex column it takes what the header and footer leave. In a parent with no height of its own it is as tall as its content.
Following the endLink to "Following the end"
With follow, the end stays in view while content is added. Scrolling up lets go (and onAtBottomChange(false) is called); scrolling back to the end, or handle.scrollToEnd(), follows again (onAtBottomChange(true)). Without follow, new content arrives below and onAtBottomChange still reports where the view is.
OverlayLink to "Overlay"
overlay sits over the area at the bottom centre and does not scroll. It is outside the named region. The area is an OverlayFrame, which places it.
Scrolling from outsideLink to "Scrolling from outside"
handle.scrollToEnd() goes to the end. handle.scrollIntoView(element, { align }) scrolls only the area, never the page around it, so element is in view: nearest (the least scrolling, the default), start or center. It works in right-to-left text.
DirectionLink to "Direction"
direction is vertical (default), horizontal or both; the other axis is clipped.
PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
label | Accessible name. With it the area is a named region, and a tab stop whenever there is something to scroll, so a keyboard can scroll it. Give one unless the content has its own focusable elements (links, buttons, a list). | string | |
direction | Which way it scrolls; content is clipped the other way. Default "vertical". | "horizontal" | "vertical" | "both" | "vertical" |
follow | Keep the end in view while content is added (a log, a chat, an activity stream). Scrolling up lets go; scrolling back to the end, or handle.scrollToEnd(), follows again. | boolean | |
onScroll | Called on every scroll of the area, with the scroll event of its scrolling element. | ((event: UIEvent<HTMLDivElement>) => void) | |
onAtBottomChange | Called with false when the view leaves the end (the bottom) and true when it is back, from scrolling or from content growing past it. It starts true. Use it to show a "jump to latest" control. | ((atBottom: boolean) => void) | |
overlay | Content pinned over the area at the bottom centre that does not scroll: a "3 new" pill, a "jump to latest" button. It is outside the named region. | ReactNode | |
handle | scrollToEnd and scrollIntoView, for scrolling from outside. | Ref<ScrollAreaHandle> | |
className | Added to the root element, for layout. | string | |
style | Layout only (e.g. maxBlockSize). Restyle through --cy-scroll-area-* tokens. | CSSProperties | |
children | What scrolls. | ReactNode | |
data-testid | Set on the root element, for tests. | string |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
- Name and focus: with
label, the scrolling element is a region of that name, and a tab stop while there is something to scroll. With nothing to scroll it is not a tab stop. Withoutlabelit is neither, so leavelabelout only when the content has its own focusable elements: focusing them scrolls the area.
| Key (region focused) | Does |
|---|---|
| Page Up / Down | Scroll a screenful |
| Home / End | Top / end |
- Shadow roots: following and the overlay work inside a shadow root.
TouchLink to "Touch"
A finger pans the area with the browser's own scrolling; nothing intercepts it. Controls in the overlay keep their own hit areas.
ThemingLink to "Theming"
No tokens of its own: the overlay's place comes from OverlayFrame's --cy-overlay-frame-offset (space between the bottom edge and the overlay, default --cy-space-4) and --cy-overlay-frame-layer. The area itself draws nothing: give the parent its background and border.
RelatedLink to "Related"
VirtualList · Stack · SplitPane · CodeBlock
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.