Content
Scroll View
Scroll content with Apple's overlay scroll bars and edges that fade while there's more to see.
import { ScrollView } from "macbetter/scroll-view"
const notes = [
{ version: "26.1", date: "Nov 12", text: "Lists remember their scroll position when you come back from a detail view." },
{ version: "26.0.3", date: "Oct 29", text: "Tables keep their selection when the sort order changes." },
{ version: "26.0.2", date: "Oct 15", text: "Outline views expand every child when you Option-click a disclosure triangle." },
{ version: "26.0.1", date: "Oct 2", text: "Carousels pause rotation while the pointer is over them." },
{ version: "26.0", date: "Sep 15", text: "Overlay scrollbars widen under the pointer and fade once you stop scrolling." },
{ version: "25.4", date: "Aug 20", text: "Disclosure groups animate their height with the system spring." },
{ version: "25.3", date: "Jul 30", text: "Cards use continuous corners where the browser supports them." },
]
export default function Example() {
return (
<ScrollView aria-label="Release notes" variant="bordered" style={{ width: "100%", maxWidth: 420, height: 260 }}>
<div style={{ display: "flex", flexDirection: "column", gap: 14, padding: 16 }}>
{notes.map((note) => (
<article key={note.version}>
<div style={{ display: "flex", justifyContent: "space-between", font: "var(--mb-text-headline)" }}>
<span>Version {note.version}</span>
<span style={{ color: "var(--mb-label-secondary)", fontWeight: 400 }}>{note.date}</span>
</div>
<div style={{ marginTop: 2, color: "var(--mb-label-secondary)" }}>{note.text}</div>
</article>
))}
</div>
</ScrollView>
)
}When to use it
Use a scroll view when a region of a page scrolls on its own: a sidebar, a log, a table under a fixed header, a row of albums. The page itself scrolls with the browser's own scroll bars; a scroll view is for the parts inside it.
- Give it a fixed height (or width, sideways) so it has something to scroll.
- Name it with
aria-labelwhen it holds content people read, so it becomes a landmark they can reach. - Don't nest scroll views that scroll the same way.
Usage
import { ScrollView } from "macbetter/scroll-view"ScrollView puts its children in a viewport from ui-sdk's Scroll Area and draws the scroll bars for you. viewportProps reach the element that scrolls, like tabIndex or onScroll.
Sideways
orientation="horizontal" scrolls sideways; "both" scrolls both ways, with a corner where the bars meet.
import { ScrollView } from "macbetter/scroll-view"
const albums = [
{ id: "dunes", title: "Dunes", color: "var(--mb-orange)" },
{ id: "forest", title: "Forest", color: "var(--mb-green)" },
{ id: "glacier", title: "Glacier", color: "var(--mb-cyan)" },
{ id: "canyon", title: "Canyon", color: "var(--mb-red)" },
{ id: "meadow", title: "Meadow", color: "var(--mb-mint)" },
{ id: "harbor", title: "Harbor", color: "var(--mb-blue)" },
{ id: "dusk", title: "Dusk", color: "var(--mb-indigo)" },
]
export default function Example() {
return (
<ScrollView aria-label="Albums" orientation="horizontal" style={{ width: "100%", maxWidth: 520 }}>
<div style={{ display: "flex", gap: 12, padding: "4px 0 14px" }}>
{albums.map((album) => (
<figure key={album.id} style={{ margin: 0, width: 120 }}>
<div style={{ height: 120, borderRadius: 12, background: `linear-gradient(160deg, ${album.color}, color-mix(in oklch, ${album.color} 55%, black))` }} />
<figcaption style={{ marginTop: 6, font: "var(--mb-text-subheadline)" }}>{album.title}</figcaption>
</figure>
))}
</div>
</ScrollView>
)
}Edge fades
Content fades out at an edge while there's more to scroll that way, and the fade shrinks to nothing as you reach the end, like the scroll edge effect under iOS bars. Turn it off with fade={false}.
Frames
variant="bordered" draws a hairline frame with rounded corners, the bordered scroll view a macOS table or text view sits in.
Platform behavior
- macOS. The overlay scroll bar: a 7-point rounded thumb that appears while you scroll or point at the view, fades out once you stop, and widens on a faint track when the pointer is over it, ready to drag.
- iOS and iPadOS. A 3-point thumb that shows only while the content moves, with no track and no hover. Scrolling is the browser's, with its momentum and rubber banding.
- In right-to-left layouts the vertical scroll bar sits on the left, and horizontal fades follow the reading direction.
- The thumb is a translucent black in light appearance and a translucent white in dark, so it reads on any content.
Accessibility
- The viewport is the browser's own scroller, so the arrow keys, Page Up, Page Down, Space and the trackpad all work, and screen readers read the content in order.
- A named scroll view is a region landmark. Make an unnamed one focusable with
viewportProps={{ tabIndex: 0 }}if it holds no focusable content, so keyboard users can scroll it. - The scroll bars are pointer affordances and are hidden from screen readers.
API
ScrollView
Takes the props of ui-sdk's Scroll Area, including type, scrollHideDelay, stickToEnd and controller, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "both" | "vertical" | The axes that scroll. |
fade | boolean | true | Fades content at an edge while there's more that way. |
variant | "plain" | "bordered" | "plain" | Draws a hairline frame. |
type | "auto" | "always" | "scroll" | "hover" | "hover" | When the scroll bars show. |
viewportProps | ComponentProps<typeof ScrollAreaViewport> | — | Props for the element that scrolls. |
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-orientation, data-fade, data-variant | .mb-scroll-view | The props above. |
data-visible, data-dragging | .mb-scroll-view-scrollbar | The bar is showing, or its thumb is being dragged. |
--mb-scroll-view-thumb, --mb-scroll-view-thumb-hover | platform | The thumb's thickness, at rest and under the pointer. |
--mb-scroll-view-fade | platform | How long the edge fades grow. |