Primitives Layout and display
Scroll area
A scrolling region the keyboard can reach. The browser scrolls and draws the scrollbars; tabindex and a name make the region a tab stop a screen reader announces, and the stylesheet thins the scrollbars and shades the edges that have more content. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
overflow: auto,tabindex="0",scrollbar-width,scrollbar-color,scrollbar-gutter,overscroll-behavior,animation-timeline: scroll()- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
overflow: auto + tabindex="0" in role="region"
On this page
Example
2.4
Offline sync for notes; conflicts are shown side by side.
2.3
Shared folders with read-only members.
2.2
Search across attachments, including scanned pages.
2.1
Keyboard shortcuts for every command in the editor.
2.0
A new editor with tables, checklists and code blocks.
1.9
Exports to Markdown and HTML.
1.8
Reminders on notes, with a daily digest.
<div
role="region"
aria-label="Release notes"
tabindex="0"
data-slean="scroll-area"
data-variant="outline"
class="notes"
>
<h3>2.4</h3>
<p>Offline sync for notes; conflicts are shown side by side.</p>
<h3>2.3</h3>
<p>Shared folders with read-only members.</p>
<h3>2.2</h3>
<p>Search across attachments, including scanned pages.</p>
<h3>2.1</h3>
<p>Keyboard shortcuts for every command in the editor.</p>
<h3>2.0</h3>
<p>A new editor with tables, checklists and code blocks.</p>
<h3>1.9</h3>
<p>Exports to Markdown and HTML.</p>
<h3>1.8</h3>
<p>Reminders on notes, with a daily digest.</p>
</div>
<style>
/* The page gives the area its size. */
.notes {
max-block-size: 14rem;
padding: 1rem;
}
</style><script lang="ts">
import '@svelte-lean/styles/scroll-area.css';
</script>
<div
role="region"
aria-label="Release notes"
tabindex="0"
data-slean="scroll-area"
data-variant="outline"
class="notes"
>
<h3>2.4</h3>
<p>Offline sync for notes; conflicts are shown side by side.</p>
<h3>2.3</h3>
<p>Shared folders with read-only members.</p>
<h3>2.2</h3>
<p>Search across attachments, including scanned pages.</p>
<h3>2.1</h3>
<p>Keyboard shortcuts for every command in the editor.</p>
<h3>2.0</h3>
<p>A new editor with tables, checklists and code blocks.</p>
<h3>1.9</h3>
<p>Exports to Markdown and HTML.</p>
<h3>1.8</h3>
<p>Reminders on notes, with a daily digest.</p>
</div>
<style>
/* The page gives the area its size. */
.notes {
max-block-size: 14rem;
padding: 1rem;
}
</style>Why this implementation exists
Every scrolling region is already a native component: overflow: auto scrolls, the browser draws the scrollbars, and the arrow keys, PageDown, Space, Home and End scroll a focused region. What a region with only text inside lacks is a way to receive focus in every browser, and a name for the screen reader that lands on it. tabindex="0" and role="region" with aria-label give it both.
Libraries replace the scrollbar with elements of their own to style it; the platform now styles the real one. scroll-area.css sets scrollbar-width: thin, scrollbar-color from the tokens and scrollbar-gutter: stable, contains overscroll on the scrolling axis, and, where scroll-driven animations exist, drives inset shadows from the region’s own scroll timeline.
The browser owns
- scrolling, the scrollbars and their dragging
- the scrolling keys: arrows, PageUp, PageDown, Space, Home, End
- the tab stop from tabindex="0" and the announcement of the named region
- scroll chaining and its containment, the scroll timeline behind the shadows
Svelte Lean owns
- scroll-area.css: thin token-colored scrollbars, a stable gutter, overscroll contained on one axis
- the edge shadows under @supports, mirrored under RTL
- the contract: the name, the role and the tab stop a scrolling region needs
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/scroll-area.css';Give the region its size from the page: a max-block-size for a vertical area, content wider than the region for a horizontal one. Name it with aria-label or aria-labelledby pointing at the heading above it.
Use role="group" instead of region when a page has many small scroll areas: each named region is a landmark. Leave tabindex="0" in place either way.
data-variant="outline" adds a border and the surface color. The shadows are drawn under the content, so content with its own opaque background covers them.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div role="region" aria-label="…" tabindex="0" data-slean="scroll-area"> | – | yes | data-slean-orientation="vertical|horizontal"; data-variant="outline". The page sets its block size or max. |
Runtime profile
Tier 0: the scroll area has no behavior module, the Vite plugin maps scroll-area to no module, and the page ships no Svelte Lean JavaScript for it.
Accessibility contract
tabindex="0"makes the region a tab stop in every browser, so a keyboard user can scroll it when it holds nothing focusable.role="region"(orgroup) with a name: a focusable element needs one, and it is announced when focus lands on the region.- The scrolling keys are the browser’s: arrows, PageUp, PageDown, Space, Home and End.
- The scrollbars are the platform’s, so their pointer behavior, their size in the system settings and forced colors are unchanged.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab | anywhere | Reaches the region (tabindex="0") |
| ArrowDown/ArrowUp | focus on the region | Scrolls by a line (native) |
| ArrowRight/ArrowLeft | focus on the region | Scrolls sideways by a line (native) |
| PageDown/PageUp/Space | focus on the region | Scrolls by a page (native) |
| Home/End | focus on the region | Scrolls to the start or the end (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
scrollbar-width | Baseline 2024 (Safari 18.2) | Safari before 18.2 draws its default scrollbar |
scrollbar-color | Baseline 2025 (Safari 26.2) | Safari before 26.2 keeps the system colors |
scrollbar-gutter | Baseline 2024 | The content shifts when the scrollbar appears |
overscroll-behavior: contain | Not Baseline (Chrome 63, Firefox 59, Safari 16 on scrolling containers) | The page scrolls on when the area reaches its end |
Scroll-driven animations | Not Baseline (Chrome 115, Safari 26) | No edge shadows; the area scrolls the same |
Without JavaScript
Fully functional: scrolling, the keyboard and the scrollbars are native.
Server rendering
Static markup. The region starts at the beginning of its content.
Before hydration
Nothing is attached; the area scrolls before and after hydration alike.
Styling
scroll-area.css sets the scrollbar width, colors and gutter, the overscroll containment per axis, the outline variant and the scroll-driven edge shadows. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-border-strong | var(--slean-neutral-8) | scrollbar-color |
--slean-radius-md | 0.625rem | border-radius |
--slean-border | var(--slean-neutral-6) | border |
--slean-surface | oklch(100% 0 0) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-overlay | oklch(0% 0 0 / 0.4) | box-shadow |
State selectors the stylesheet targets, all from the platform or ARIA: :dir(rtl).
Variant attributes: data-slean-orientation (horizontal); data-variant (outline).
Compatibility notes
scrollbar-width and scrollbar-gutter are Baseline 2024 (Safari 18.2) and scrollbar-color Baseline 2025 (Safari 26.2); older Safari keeps its default scrollbar. overscroll-behavior works on scrolling containers in every current engine but is not listed as Baseline, because engines differ on containers without overflow. Scroll-driven animations are not Baseline (Chrome 115, Safari 26, not Firefox): without them there are no edge shadows.
Examples
Horizontal
data-slean-orientation="horizontal" contains overscroll along the inline axis only, so
a vertical wheel over the row still scrolls the page, and moves the edge shadows to the start and
end sides. Focus the row and use ArrowRight, or scroll it sideways.
- Harbor
- Lighthouse
- Breakwater
- Tidewater
- Mooring
- Quayside
<div
role="region"
aria-label="Recent projects"
tabindex="0"
data-slean="scroll-area"
data-slean-orientation="horizontal"
>
<ul class="row">
<li>Harbor</li>
<li>Lighthouse</li>
<li>Breakwater</li>
<li>Tidewater</li>
<li>Mooring</li>
<li>Quayside</li>
</ul>
</div>Testing
apps/playground/tests/primitives/display-extra.spec.tsPlaywright, with page JavaScript enabled and disabled: roles, names, keyboard, geometry, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/scroll-area/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/scroll-area/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/scroll-area.cssthe optional stylesheet