Scroll area
Content that scrolls inside a fixed height, as the product’s lists do: no scrollbar of the system, a fade at the edge that has more, and a thin indicator that shows while it scrolls and never covers a row or its buttons.
- Native HTML
- 2.0 KB gzipped · script and styles
@orbex/sonar-components/scroll-area
Examples
Invoices
Code
import { Card, CardLink } from '@orbex/sonar-components/card'
import { ScrollArea } from '@orbex/sonar-components/scroll-area'
import type { ComponentProps } from 'react'
import downloadSymbol from '@material-symbols/svg-400/rounded/download.svg?raw'
/* Material Symbols Rounded, weight 400, as icon components (Foundations › Icons) */
const symbol = (svg: string) => {
const path = svg.match(/ d="([^"]+)"/)?.[1]
return (props: ComponentProps<'svg'>) => (
<svg viewBox="0 -960 960 960" fill="currentColor" aria-hidden="true" focusable="false" {...props}>
<path d={path} />
</svg>
)
}
const DownloadIcon = symbol(downloadSymbol)
const invoices = [
['Tuesday, May 12', 'INV-0025698', '04-118B'],
['Monday, May 11', 'INV-0025677', '04-121A'],
['Monday, May 11', 'INV-0025671', '04-130A'],
['Friday, May 8', 'INV-0025664', '04-112B'],
['Thursday, May 7', 'INV-0025642', '04-109C'],
['Wednesday, May 6', 'INV-0025631', '04-104A'],
] as const
/* The Invoices card: the list scrolls between the heading and the footer. Its first rule is the heading's (1.0.0-alpha.23:
the list drew a second one under it), as in the product, where the rule is the list's first line. No scrollbar of the system: the indicator
runs in the card's padding, beside the download buttons, never over them */
export default function Invoices() {
return (
<Card titleAs="h3" title="Invoices" size="m" footer={<CardLink href="/patterns/cards/#optional">Go to Invoices</CardLink>}>
<ScrollArea label="Invoices" style={{ flex: '1 1 0' }}>
<ul role="list" style={{ margin: 0, padding: 0 }}>
{invoices.map(([date, number, order]) => (
<li key={number} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, height: 66, borderBottom: '1px solid var(--sonar-color-divider)' }}>
<span style={{ font: '400 12px/16px var(--sonar-font-family-sans)', color: 'var(--sonar-color-text-meta)' }}>
{date}
<br />
<strong style={{ color: 'var(--sonar-color-text-strong)', fontWeight: 600 }}>{number}</strong>
<br />
{`Order ${order}`}
</span>
<button
type="button"
aria-label={`Download ${number}`}
style={{ display: 'grid', placeItems: 'center', width: 24, height: 24, padding: 0, border: 0, borderRadius: '50%', background: 'none', color: 'var(--sonar-color-accent)', boxShadow: 'inset 0 0 0 1px var(--sonar-color-accent)' }}
>
<DownloadIcon style={{ width: 15, height: 15 }} />
</button>
</li>
))}
</ul>
</ScrollArea>
</Card>
)
}
Code
import { ScrollArea } from '@orbex/sonar-components/scroll-area'
const updates = ['2.03 · Route progress on the delivery map', '2.02 · Recurring orders from favorites', '2.01 · Switches for the modules', '2.00 · Dark mode', '1.12 · Invoices from the overview', '1.11 · Help on delivery alerts', '1.10 · Invoices in bulk', '1.09 · Driver contact on delivery notes', '1.08 · Last quantities per product']
/* No padding beside the area: the indicator takes a lane of its own at the end, which the rows never reach */
export default function Inside() {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 220, width: 'min(100%, 360px)', borderRadius: 'var(--sonar-shape-corner-medium)', background: 'var(--sonar-color-surface-card)', boxShadow: 'var(--sonar-elevation-1)' }}>
<ScrollArea label="System updates" indicator="inside">
<ul role="list" style={{ margin: 0, padding: '4px 16px' }}>
{updates.map((update) => (
<li key={update} style={{ padding: '12px 0', borderBottom: '1px solid var(--sonar-color-divider)', font: '400 12px/16px var(--sonar-font-family-sans)', color: 'var(--sonar-color-text-strong)' }}>
{update}
</li>
))}
</ul>
</ScrollArea>
</div>
)
}
Usage
- For every list that scrolls inside something else: the lists of the optional cards, the bodies of side panels and sheets, the System notifications. The page itself keeps the browser’s scroll.
- A list stops on its rows, scrolling down or up. In a card, a row that stops at the top lies on the title’s rule, with as much room above its content as below it; the last stop mirrors the first, and every stop shows whole rows. The fades cover only the room inside a row (or between bands), never a row that has stopped.
- The indicator is 2 px, in the scrollbar color of each theme, never shorter than 32 px. It shows while scrolling and fades out a second after; it flashes for 0.8 s when the pointer or the keyboard arrives, to say there is more.
- Outside (the default): it runs 4 to 6 px past the end of the area, in the padding of the card or panel around it. Inside: when there is no padding, it takes a 12 px lane at the end of the area, and the content stops before it.
- Never a system scrollbar over the content: on phones it was drawn over the download buttons, white in dark mode.
- A card’s own list that gives way first is a Card scroll, built on the same behavior.
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Reaches the area only while it scrolls; when everything fits, focus goes straight to its controls. |
| Up and Down arrows, Page Up and Page Down | Scroll the area once it has focus. |
Roles and attributes
| Element or attribute | What it gives |
|---|---|
role="group" while scrolling |
Named by label (“Invoices”), so focus arriving on it says what it is. |
| Fade and indicator | Drawn, aria-hidden; the area’s own scroll position is what assistive technology reads. No system scrollbar is drawn. |
Notes
- While it scrolls, the area is a named group that takes focus, so the arrow keys, Page Up and Page Down scroll it. When everything fits, it adds no tab stop.
- Label it with what it holds: “Invoices”, “What’s new”.
- The indicator is decoration, hidden from assistive technology; with reduced motion it appears and goes without fading.
Use it
Each component is its own entry point with its own stylesheet, so an app loads only what it uses. The tokens and the cascade layers are set once for the app: see Installation.
import { ScrollArea } from '@orbex/sonar-components/scroll-area'
import '@orbex/sonar-components/scroll-area.css'
API
| Prop | Type | Default |
|---|---|---|
label
Required
What the content is, read when the area takes focus to scroll: “Invoices”, “System notifications”. |
string
|
— |
indicator
Where the indicator runs. Outside: in the padding beside the area, 4 to 6 px past its end, as in the product’s cards and panels, where there is padding to hold it. Inside: in a lane of its own at the end of the area, which the content never reaches, for an area with no padding beside it. |
'outside' | 'inside'
|
'outside' |
Also takes every attribute of the native <div>
, and a ref to it.