Skip to main content

Scroll Area

A scrollable region with a custom, theme-aware scrollbar that appears on hover or while scrolling.

Releases

  • v2.24.0Security fixes
  • v2.23.0Improvements
  • v2.22.0Improvements
  • v2.21.0Security fixes
  • v2.20.0Improvements
  • v2.19.0Improvements
  • v2.18.0Security fixes
  • v2.17.0Improvements
  • v2.16.0Improvements
  • v2.15.0Security fixes
  • v2.14.0Improvements
  • v2.13.0Improvements
  • v2.12.0Security fixes
  • v2.11.0Improvements
  • v2.10.0Improvements
  • v2.9.0Security fixes
  • v2.8.0Improvements
  • v2.7.0Improvements
  • v2.6.0Security fixes
  • v2.5.0Improvements
  • v2.4.0Improvements
  • v2.3.0Security fixes
  • v2.2.0Improvements
  • v2.1.0Improvements
1import { ScrollArea } from '@/components/ui/scroll-area';
2
3const versions = Array.from({ length: 24 }, (_, index) => {
4 const minor = 24 - index;
5 return { tag: `v2.${minor}.0`, note: minor % 3 === 0 ? 'Security fixes' : 'Improvements' };
6});
7
8export function Default() {
9 return (
10 <ScrollArea className="border-border bg-card h-72 w-56 rounded-lg border">
11 <div className="p-3">
12 <p className="text-muted-foreground mb-3 px-1 text-xs font-medium tracking-wide uppercase">
13 Releases
14 </p>
15 <ul className="flex flex-col">
16 {versions.map((version) => (
17 <li
18 key={version.tag}
19 className="border-border flex items-center justify-between border-b px-1 py-2 text-sm last:border-0"
20 >
21 <span className="font-medium">{version.tag}</span>
22 <span className="text-muted-foreground text-xs">{version.note}</span>
23 </li>
24 ))}
25 </ul>
26 </div>
27 </ScrollArea>
28 );
29}

Installation

pnpm dlx nachui add scroll-area

Anatomy

1import { ScrollArea } from '@/components/ui/scroll-area';
1<ScrollArea className="h-72 w-56 rounded-md border">
2 <div className="p-4">Long content</div>
3</ScrollArea>
The root wraps your content in a viewport that scrolls natively with the browser scrollbar hidden, then draws its own thumb on top. Wheel, touch, keyboard and drag all keep working because the viewport is a regular overflowing element. The thumb is dragged with the pointer and the track jumps on click.

Composition

ScrollArea
└── ScrollArea.Bar
The root renders a Bar for each direction in orientation. Add a Bar yourself with forceMount when you need one that never hides.

Variants

Default

Vertical scrolling. With type="hover" (the default) the bar shows while the pointer is over the area or the content is scrolling.

Releases

  • v2.24.0Security fixes
  • v2.23.0Improvements
  • v2.22.0Improvements
  • v2.21.0Security fixes
  • v2.20.0Improvements
  • v2.19.0Improvements
  • v2.18.0Security fixes
  • v2.17.0Improvements
  • v2.16.0Improvements
  • v2.15.0Security fixes
  • v2.14.0Improvements
  • v2.13.0Improvements
  • v2.12.0Security fixes
  • v2.11.0Improvements
  • v2.10.0Improvements
  • v2.9.0Security fixes
  • v2.8.0Improvements
  • v2.7.0Improvements
  • v2.6.0Security fixes
  • v2.5.0Improvements
  • v2.4.0Improvements
  • v2.3.0Security fixes
  • v2.2.0Improvements
  • v2.1.0Improvements
1import { ScrollArea } from '@/components/ui/scroll-area';
2
3const versions = Array.from({ length: 24 }, (_, index) => {
4 const minor = 24 - index;
5 return { tag: `v2.${minor}.0`, note: minor % 3 === 0 ? 'Security fixes' : 'Improvements' };
6});
7
8export function Default() {
9 return (
10 <ScrollArea className="border-border bg-card h-72 w-56 rounded-lg border">
11 <div className="p-3">
12 <p className="text-muted-foreground mb-3 px-1 text-xs font-medium tracking-wide uppercase">
13 Releases
14 </p>
15 <ul className="flex flex-col">
16 {versions.map((version) => (
17 <li
18 key={version.tag}
19 className="border-border flex items-center justify-between border-b px-1 py-2 text-sm last:border-0"
20 >
21 <span className="font-medium">{version.tag}</span>
22 <span className="text-muted-foreground text-xs">{version.note}</span>
23 </li>
24 ))}
25 </ul>
26 </div>
27 </ScrollArea>
28 );
29}

Horizontal

orientation="horizontal" hides vertical overflow and draws the bar along the bottom edge.
Virginiaus-east-112 ms
Oregonus-west-248 ms
Frankfurteu-central-196 ms
Sao Paulosa-east-1140 ms
Singaporeap-southeast-1210 ms
Sydneyap-southeast-2245 ms
Tokyoap-northeast-1180 ms
1import { ScrollArea } from '@/components/ui/scroll-area';
2
3const regions = [
4 { city: 'Virginia', code: 'us-east-1', latency: '12 ms' },
5 { city: 'Oregon', code: 'us-west-2', latency: '48 ms' },
6 { city: 'Frankfurt', code: 'eu-central-1', latency: '96 ms' },
7 { city: 'Sao Paulo', code: 'sa-east-1', latency: '140 ms' },
8 { city: 'Singapore', code: 'ap-southeast-1', latency: '210 ms' },
9 { city: 'Sydney', code: 'ap-southeast-2', latency: '245 ms' },
10 { city: 'Tokyo', code: 'ap-northeast-1', latency: '180 ms' },
11];
12
13export function Horizontal() {
14 return (
15 <ScrollArea orientation="horizontal" className="w-full max-w-md">
16 <div className="flex gap-3 p-1 pb-3">
17 {regions.map((region) => (
18 <div
19 key={region.code}
20 className="border-border bg-card flex w-36 shrink-0 flex-col gap-1 rounded-lg border p-3"
21 >
22 <span className="text-sm font-medium">{region.city}</span>
23 <span className="text-muted-foreground font-mono text-xs">{region.code}</span>
24 <span className="text-muted-foreground text-xs">{region.latency}</span>
25 </div>
26 ))}
27 </div>
28 </ScrollArea>
29 );
30}

Both directions

orientation="both" for wide tables. type="always" keeps the bars visible whenever the content overflows.
RouteMethodp50p95p99ErrorsRequestsRegion
/api/ordersGET32 ms120 ms340 ms0.1%1.2Mus-east-1
/api/ordersPOST58 ms210 ms610 ms0.4%240Kus-east-1
/api/customersGET21 ms88 ms190 ms0.0%980Keu-central-1
/api/invoicesGET44 ms160 ms420 ms0.2%410Keu-central-1
/api/invoices/:id/pdfGET310 ms900 ms1.8 s1.1%32Kus-west-2
/api/webhooksPOST12 ms40 ms95 ms0.0%3.4Map-southeast-1
/api/searchGET80 ms260 ms700 ms0.3%620Kus-east-1
/api/exportsPOST1.2 s3.4 s6.1 s2.0%8Ksa-east-1
/api/auth/sessionGET9 ms30 ms70 ms0.0%5.1Mus-east-1
/api/auth/loginPOST140 ms400 ms900 ms0.8%150Kus-east-1
1import { ScrollArea } from '@/components/ui/scroll-area';
2
3const columns = ['Route', 'Method', 'p50', 'p95', 'p99', 'Errors', 'Requests', 'Region'];
4
5const rows = [
6 ['/api/orders', 'GET', '32 ms', '120 ms', '340 ms', '0.1%', '1.2M', 'us-east-1'],
7 ['/api/orders', 'POST', '58 ms', '210 ms', '610 ms', '0.4%', '240K', 'us-east-1'],
8 ['/api/customers', 'GET', '21 ms', '88 ms', '190 ms', '0.0%', '980K', 'eu-central-1'],
9 ['/api/invoices', 'GET', '44 ms', '160 ms', '420 ms', '0.2%', '410K', 'eu-central-1'],
10 ['/api/invoices/:id/pdf', 'GET', '310 ms', '900 ms', '1.8 s', '1.1%', '32K', 'us-west-2'],
11 ['/api/webhooks', 'POST', '12 ms', '40 ms', '95 ms', '0.0%', '3.4M', 'ap-southeast-1'],
12 ['/api/search', 'GET', '80 ms', '260 ms', '700 ms', '0.3%', '620K', 'us-east-1'],
13 ['/api/exports', 'POST', '1.2 s', '3.4 s', '6.1 s', '2.0%', '8K', 'sa-east-1'],
14 ['/api/auth/session', 'GET', '9 ms', '30 ms', '70 ms', '0.0%', '5.1M', 'us-east-1'],
15 ['/api/auth/login', 'POST', '140 ms', '400 ms', '900 ms', '0.8%', '150K', 'us-east-1'],
16];
17
18export function Both() {
19 return (
20 <ScrollArea
21 orientation="both"
22 type="always"
23 className="border-border bg-card h-56 w-full max-w-md rounded-lg border"
24 >
25 <table className="w-max min-w-full text-left text-sm">
26 <thead className="bg-muted/60 text-muted-foreground sticky top-0 text-xs">
27 <tr>
28 {columns.map((column) => (
29 <th key={column} className="px-3 py-2 font-medium whitespace-nowrap">
30 {column}
31 </th>
32 ))}
33 </tr>
34 </thead>
35 <tbody>
36 {rows.map((row) => (
37 <tr key={`${row[0]}-${row[1]}`} className="border-border border-t">
38 {row.map((cell, index) => (
39 <td
40 key={columns[index]}
41 className={
42 index === 0
43 ? 'px-3 py-2 font-mono whitespace-nowrap'
44 : 'px-3 py-2 whitespace-nowrap'
45 }
46 >
47 {cell}
48 </td>
49 ))}
50 </tr>
51 ))}
52 </tbody>
53 </table>
54 </ScrollArea>
55 );
56}

API Reference

ScrollArea

PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "both""vertical"Which directions scroll
type"auto" | "always" | "hover" | "scroll""hover"When the bar is visible. All of them need overflow
scrollHideDelaynumber600Milliseconds before the bar hides after scrolling
viewportRefReact.Ref<HTMLDivElement>-Ref to the scrolling element, for scrollTo and alike
viewportClassNamestring-Classes for the viewport
classNamestring-Classes for the root. Give it a height or a width

ScrollArea.Bar

PropTypeDefaultDescription
orientation"vertical" | "horizontal""vertical"Which axis the bar controls
forceMountbooleanfalseKeep it visible regardless of type
classNamestring-Additional CSS classes
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I