---
title: Scroll Area
description: The named inner scroll: thin bar, and the smooth scroller handed back - the pair everyone forgets separately.
source: https://adamjurek.com/components/scroll-area
---

## Home


`ScrollArea` wraps a bounded region - a changelog panel, a code block, a list
inside a card - in the site's thin scrollbar, and hands the wheel back to the
page's smooth scroller the moment the cursor leaves it. Reach for it whenever
a piece of content needs its own scroll, not the page's.

<!-- ::start:showcase demo="scroll-area" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

Two behaviours were being forgotten separately often enough to earn one name:
the site's thin scrollbar styling, and `data-lenis-prevent`, which tells the
smooth scroller to leave this subtree's wheel and touch events alone. Skip
either one and the region either looks like an unstyled native scrollbar or
fights the page for the same gesture.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/scroll-area.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/scroll-area.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | layout · Containers |
| Files | `scroll-area.tsx` |
| Dependencies | None |
| Tags | scroll, lenis, no-deps |

> [!NOTE] No runtime dependencies
> It brings nothing with it beyond the stylesheet.

## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { ScrollArea } from "@sushindustries/ui";

export function ChangelogPanel() {
	return (
		<ScrollArea maxHeight="16rem">
			<ul className="flex flex-col gap-2">
				<li>v0.9 - Pagination clamps out-of-range pages.</li>
				<li>v0.8 - Sheet gained a `side` prop.</li>
				<li>v0.7 - Reveal never un-reveals.</li>
			</ul>
		</ScrollArea>
	);
}
```

## What you should see

Content clipped to `maxHeight`, scrollable inside its own box once it
overflows, with the site's thin scrollbar rather than the browser default.
Scrolling inside it doesn't move the rest of the page - the wheel is handed
back to this container and released once you reach its top or bottom.

## If nothing happens

If the inner content scrolls the whole page instead of staying contained,
something above `ScrollArea` in the tree is intercepting scroll before it
gets here - the component always sets `data-lenis-prevent` itself, so that
isn't the missing piece. If there's no scrollbar at all, the content is
probably shorter than `maxHeight`, and that's correct.


## Guides


## When not to use it

For the page's own vertical scroll - that's Lenis's job, and wrapping the
whole page (or a large section of normal page flow) in `ScrollArea` fights
the smooth scroller instead of cooperating with it. Reach for it only for a
genuinely separate, bounded region: a changelog panel, a code block, a list
inside a card.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `children` | `ReactNode` | - |  |
| `maxHeight?` | `string` | `"20rem"` | CSS max-height; scrolling starts past it. |

<!-- /generated:api -->

## Notes

`data-lenis-prevent` is always set, not conditional on `maxHeight` or on
whether the content actually overflows - a `ScrollArea` shorter than its
content costs nothing extra by carrying the attribute unused.

`maxHeight` takes any CSS length, not just the scale's tokens - `"50vh"` and
`"320px"` are both valid, since a scroll boundary is one of the few places a
literal value is the point rather than the drift a token exists to prevent.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="scroll-area" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { ScrollArea } from "@sushindustries/ui";

interface Item {
	id: string;
	label: string;
}

export function OrderSummary({ items }: { items: readonly Item[] }) {
	return (
		<div className="card p-4">
			<h3 className="h4 m-0">Your order</h3>
			<div className="mt-3">
				<ScrollArea maxHeight="12rem">
					<ul className="flex flex-col gap-2">
						{items.map((item) => (
							<li key={item.id}>{item.label}</li>
						))}
					</ul>
				</ScrollArea>
			</div>
		</div>
	);
}
```

## What this example is not

Sized for any list length. `maxHeight="12rem"` is a fixed choice for this
card; a list expected to run to hundreds of items wants a real virtualised
list inside the area, not just a scrollbar on top of all of them at once.
