Skip to content
Drivn
6 min read

React Scroll Area Component Examples

Copy-paste React Scroll Area examples: vertical list, horizontal card row, sizing rules, nested card panel, and scrollbar styling. Zero runtime deps.

A scroll area is the right wrapper whenever a region of the UI has more content than vertical or horizontal space — a sidebar of links, a chat transcript, a long settings panel, a horizontal row of cards. The Scroll Area component in Drivn is a single <div> that turns on native browser scrolling and restyles the scrollbar with CSS, so the bar reads thin and on-theme without any JavaScript. The whole component lives in @/components/ui/scroll-area.tsx and imports nothing past react and the local cn() utility — no Radix, no scroll library.

Every example on this page imports the component from @/components/ui/scroll-area — the path the Drivn CLI installs under — and renders the same <ScrollArea className="..."> shape. Each snippet is TypeScript because Drivn ships TypeScript-only. The one rule that runs through all of them: a Scroll Area scrolls only when you give it a constrained height (or width), since a bare <div> grows to fit its content.

The examples below cover a fixed-height vertical list, a horizontal card row with the orientation prop, the sizing rules that decide whether the area scrolls at all, a Scroll Area nested inside a Card, and a look at the scrollbar styling itself. The full component reference and props table live on the Scroll Area docs page, and the shadcn/ui comparison sits on Drivn vs shadcn Scroll Area.

A fixed-height vertical list

The most common Scroll Area: a list capped at a fixed height. Set the height with a Tailwind class on classNameh-48 here caps the box at 12rem — and the component scrolls anything past that point. The list inside is plain markup; the Scroll Area does not impose a structure, it only owns the overflow and the scrollbar. Because the component spreads React.HTMLAttributes<HTMLDivElement>, any extra <div> prop — id, role, aria-label, event handlers — flows straight through.

This is the right shape for a sidebar of navigation links, a list of search results, a notification feed, or any column that could grow past the space the layout gives it. The Scroll Area renders as a server component, so a static list like this needs no 'use client' directive at all. For a list of selectable rows, pair the Scroll Area with the Drivn Command menu, which already handles keyboard navigation inside its own scroll region.

1import { ScrollArea } from "@/components/ui/scroll-area"
2
3const tags = Array.from(
4 { length: 30 },
5 (_, i) => 'Tag ' + (i + 1)
6)
7
8export default function TagList() {
9 return (
10 <ScrollArea className="h-48 w-56 rounded-md border p-3">
11 {tags.map((tag) => (
12 <div key={tag} className="py-1.5 text-sm">
13 {tag}
14 </div>
15 ))}
16 </ScrollArea>
17 )
18}

A horizontal card row

For a swipeable row of cards or thumbnails, pass orientation="horizontal". The component switches to overflow-x-auto overflow-y-hidden, so the area scrolls sideways and clips the vertical axis. The child content has to be wider than the container for anything to scroll — wrap the items in a flex row with w-max so they lay out at their natural width and overflow past the edge instead of wrapping.

This pattern fits an image gallery, a row of product tiles, a preview strip, or a horizontal set of stat cards on a dashboard. Unlike a true Carousel, a horizontal Scroll Area has no slides, no autoplay, and no snap by default — it is plain native scrolling, which is exactly right when the content is browse-anywhere rather than step-through. Add snap-x and snap-start utilities to the children if you want scroll snapping. Each tile here is a fixed-width box; swap in a Drivn Card for richer content.

1import { ScrollArea } from "@/components/ui/scroll-area"
2
3export default function ThumbnailRow() {
4 return (
5 <ScrollArea orientation="horizontal" className="w-full">
6 <div className="flex gap-3 w-max pb-3">
7 {Array.from({ length: 12 }).map((_, i) => (
8 <div
9 key={i}
10 className="h-24 w-40 shrink-0 rounded-lg border"
11 />
12 ))}
13 </div>
14 </ScrollArea>
15 )
16}

Sizing the scroll area

A Scroll Area only scrolls when its content overflows a constrained box. The component is a styled <div>, and a <div> grows to fit its content by default, so without a height nothing overflows. There are two ways to constrain it, both set through className: a fixed height like h-72, which always reserves that space, or a max-height like max-h-72, which lets the area shrink for short content and only scrolls once the content grows past the cap.

Reach for max-h-* when the content length varies — a comment thread, a filtered result list, an expandable panel — so a near-empty state does not leave a tall blank box. Reach for a fixed h-* when the layout needs a stable, predictable slot regardless of content. Drivn merges the className onto the base styles through cn(), so arbitrary values like h-[420px] and responsive prefixes like md:h-96 all work. The same rule governs horizontal scrolling, where the constraint is a width rather than a height.

1import { ScrollArea } from "@/components/ui/scroll-area"
2
3export default function SizedAreas() {
4 return (
5 <div className="space-y-4">
6 {/* Fixed height always 18rem tall */}
7 <ScrollArea className="h-72 rounded-md border p-3">
8 {/* long content */}
9 </ScrollArea>
10 {/* Max height grows up to 18rem, then scrolls */}
11 <ScrollArea className="max-h-72 rounded-md border p-3">
12 {/* variable content */}
13 </ScrollArea>
14 </div>
15 )
16}

A scrollable panel inside a Card

A Scroll Area composes cleanly inside other containers. Drop one inside a Drivn Card to keep a long body — a chat log, an activity feed, an order history — from pushing the card and the rest of the page taller. The Card supplies the border, padding, and header; the Scroll Area owns only the overflow region, so the header stays pinned while the body scrolls underneath it.

The same composition works inside a Dialog: put a max-h-* Scroll Area around the dialog body and a long form or terms block scrolls within the modal instead of growing it off-screen. Because the Scroll Area is just a <div>, it nests anywhere a <div> is valid and adds no stacking-context or focus-trap surprises. The example below wraps a message list in a Card body; the flex flex-col on the list keeps messages stacked while the Scroll Area handles the overflow.

1import { ScrollArea } from "@/components/ui/scroll-area"
2import { Card } from "@/components/ui/card"
3
4const events = [
5 { id: 1, label: 'Deployed to production' },
6 { id: 2, label: 'Merged pull request #214' },
7 { id: 3, label: 'Updated billing settings' },
8]
9
10export default function ActivityCard() {
11 return (
12 <Card className="w-80">
13 <div className="border-b p-4 font-medium">Activity</div>
14 <ScrollArea className="h-64">
15 <div className="flex flex-col gap-3 p-4 text-sm">
16 {events.map((e) => (
17 <div key={e.id}>{e.label}</div>
18 ))}
19 </div>
20 </ScrollArea>
21 </Card>
22 )
23}

How the scrollbar is styled

The Scroll Area does not render a scrollbar element — it restyles the browser's native one with CSS. Every rule lives in the styles.base string, reproduced verbatim from the registry below. Firefox reads scrollbar-width: thin and scrollbar-color, which set a slim bar tinted with the --border token on a transparent track. Chromium and Safari read the ::-webkit-scrollbar pseudo-elements: the bar is 1.5 units wide and tall, the thumb is rounded-full and bg-border, and on hover the thumb darkens to bg-muted-foreground/30.

Because the styling is plain CSS, the scrollbar follows the active theme automatically — the --border and --muted-foreground tokens flip with dark and light mode, so the bar never needs a theme-specific override. There is no prop to recolor the bar per instance; the look is intentionally uniform across every Scroll Area in the app. To change it globally, edit the styles.base string in your copy of the component — the file is yours after install. For the side-by-side with the Radix-backed alternative, see Drivn vs shadcn Scroll Area.

1// styles.base — verbatim from the registry source
2const styles = {
3 base: cn(
4 'relative [scrollbar-width:thin]',
5 '[scrollbar-color:var(--border)_transparent]',
6 '[&::-webkit-scrollbar]:w-1.5',
7 '[&::-webkit-scrollbar]:h-1.5',
8 '[&::-webkit-scrollbar-track]:bg-transparent',
9 '[&::-webkit-scrollbar-track]:rounded-full',
10 '[&::-webkit-scrollbar-thumb]:rounded-full',
11 '[&::-webkit-scrollbar-thumb]:bg-border',
12 '[&:hover::-webkit-scrollbar-thumb]:bg-muted-foreground/30',
13 '[&::-webkit-scrollbar-corner]:bg-transparent'
14 ),
15 orientations: {
16 vertical: 'overflow-y-auto overflow-x-hidden',
17 horizontal: 'overflow-x-auto overflow-y-hidden',
18 },
19}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Because it has no constrained height. A Scroll Area is a styled <div> and a <div> grows to fit its content, so nothing overflows. Add a fixed height or a max-height through className<ScrollArea className="h-48"> or className="max-h-72". The area scrolls once the content is taller than that box. For horizontal scrolling the content must instead be wider than the container.

No. The Scroll Area holds no state and calls no hooks — it is a plain function that renders one <div> — so the registry source ships without a 'use client' directive and the component renders as a server component. You only add the directive to a call site when the surrounding component runs useState, an effect, or an event handler of its own. A static list or card body needs nothing extra.

Pass orientation="horizontal" on the ScrollArea. The component switches its overflow to overflow-x-auto overflow-y-hidden, so the area scrolls sideways. The child content must be wider than the container — wrap the items in a flex row with w-max so they keep their natural width and overflow past the edge instead of wrapping onto a second line.

Yes, by editing the component. The scrollbar styling lives in the styles.base string inside @/components/ui/scroll-area.tsx — it uses the --border and --muted-foreground design tokens, which already follow the active theme. There is no per-instance prop to recolor the bar; the look is uniform by design. Since the component is copy-and-own, change the ::-webkit-scrollbar and scrollbar-color rules in your copy to restyle it globally.

Yes. The Scroll Area uses native browser scrolling, so it inherits the platform's momentum scrolling, rubber-band overscroll, and touch gestures for free — there is no JavaScript scroll handler to interfere. On mobile the thin custom scrollbar is typically hidden by the OS until the user scrolls, exactly as native scrolling behaves elsewhere in the browser.