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.
Any region that holds more than the layout gives it wants a scroll area — a link sidebar, a chat transcript, a settings panel that keeps growing, a sideways row of thumbnails. Drivn's Scroll Area answers that with one <div>. It switches on native browser scrolling and repaints the browser's own scrollbar through CSS, which is why the bar reads thin and on-theme with no JavaScript running behind it. The file at @/components/ui/scroll-area.tsx imports react and the local cn() utility and stops there — no Radix package, no scroll library, no hooks.
Every snippet below pulls from @/components/ui/scroll-area, the path the Drivn CLI writes to, and every one of them is TypeScript, because Drivn ships .tsx source and nothing else. Keep one rule in mind while reading: the component scrolls only once you constrain it. A <div> stretches to fit whatever is inside it, so without a height — or a width, on the horizontal axis — there is no overflow and no scrollbar.
Five patterns follow: a fixed-height vertical list, a horizontal card row driven by the orientation prop, the sizing choices that decide whether the area scrolls at all, a Scroll Area tucked inside a Card, and a walk through the scrollbar CSS itself. Props and the full registry source sit on the Scroll Area docs page; the Radix side-by-side is 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 className — h-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.
1 import { ScrollArea } from "@/components/ui/scroll-area" 2 3 const tags = Array.from( 4 { length: 30 }, 5 (_, i) => 'Tag ' + (i + 1) 6 ) 7 8 export 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.
1 import { ScrollArea } from "@/components/ui/scroll-area" 2 3 export 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
Overflow is the trigger, and overflow needs a boundary. The Scroll Area is a styled <div>, and a <div> sizes itself to its content, so an unconstrained one simply grows and never scrolls. Two className options fix that. A fixed height — h-72 — claims 18rem whether the content fills it or not. A cap — max-h-72 — lets the box shrink to short content and starts scrolling only once the content passes 18rem.
Pick the cap when the amount of content is unknown ahead of time: a comment thread, a filtered result set, a panel that expands. It avoids the tall empty rectangle a fixed height leaves behind on a near-empty state. Pick the fixed height when the surrounding layout needs the slot to stay put — a dashboard grid or a two-column page where a shifting box would push everything else around. Since cn() merges your classes over the base styles, arbitrary values like h-[420px] and responsive prefixes like md:h-96 both land. Horizontal areas follow the identical rule with width standing in for height.
1 import { ScrollArea } from "@/components/ui/scroll-area" 2 3 export 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.
1 import { ScrollArea } from "@/components/ui/scroll-area" 2 import { Card } from "@/components/ui/card" 3 4 const 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 10 export 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 2 const 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 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
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.