How to Add a Scroll Area to a React App
Step-by-step guide to adding a scroll area to any React app with the Drivn CLI — a copy-paste, server-safe component that styles a native scrollbar.
Reaching for a scroll area means one region of your React app has more content than the space it sits in — a sidebar of links, a chat transcript, a horizontal row of cards — and you want it to scroll inside a bordered box without the thick native scrollbar dominating the layout. You could set overflow-auto on a <div> and live with the browser's default bar, but the result rarely matches a designed interface, and restyling scrollbars by hand across Firefox, Chrome, and Safari is fiddly enough that most teams want it solved once.
Drivn solves it by copy-paste: the CLI writes a scroll-area.tsx file into your repository. The Scroll Area is a single styled <div> — no Radix, no scroll library, no state. Its styles.base object themes the native scrollbar across engines ([scrollbar-width:thin] for Firefox, a stack of [&::-webkit-scrollbar] rules for Chromium and WebKit), and an orientations map swaps overflow-y-auto for overflow-x-auto. Because it holds no state and touches no browser API, the file ships without a 'use client' directive.
This guide adds the Scroll Area to any React app — install the CLI, render a fixed-height vertical region, switch to a horizontal rail with the orientation prop, and restyle the scrollbar from the one styles object you own. It works the same in Vite + React or Next.js. For the App Router walkthrough see the Next.js Scroll Area guide.
Prerequisites
Before installing Scroll Area, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your styles, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired; for a custom setup the installation page lists the minimal config. The Scroll Area is the lightest kind of Drivn install — it imports nothing past React and the cn utility for class merging, so there is no peer component, no icon package, and no runtime dependency to version-lock. The one thing to keep in mind before you render it is that a Scroll Area only scrolls when its content overflows a constrained box, so the parent layout has to give it a height or a width to work against.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Scroll Area source file. The command prompts once for your install directory (defaulting to src/components/ui/) and writes a single scroll-area.tsx — no other files, because the component has no peer dependencies beyond React and the cn utility. No global config file is created; the result is TypeScript source you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install. Because the file is yours after install, future Drivn releases will not overwrite your edits.
1 # add the Scroll Area source file 2 npx drivn add scroll-area
Step 2 — Render a fixed-height vertical region
Import ScrollArea from your UI directory and wrap the content that should scroll. The component defaults to orientation="vertical" (overflow-y-auto overflow-x-hidden), but a <div> only scrolls once its content overflows a fixed size — so the one required step is passing a height through className. h-48 caps the box at twelve rem and scrolls anything taller; max-h-64 lets a short list stay compact and only scrolls once it grows past the cap. Because the Scroll Area holds no state and ships without a 'use client' directive, this renders fine inside a Server Component with no client boundary. Any extra props — id, aria-label, onScroll — spread onto the underlying <div> through the ...props rest. Drop it around a tag list, a comment thread, or a table of contents, or wrap it in a Drivn Card for a bounded, scrollable panel.
1 import { ScrollArea } from '@/components/ui/scroll-area' 2 3 export function TagList({ tags }: { tags: string[] }) { 4 return ( 5 <ScrollArea className="h-48 w-56 rounded-md border p-3"> 6 {tags.map((tag) => ( 7 <div key={tag} className="py-1.5 text-sm"> 8 {tag} 9 </div> 10 ))} 11 </ScrollArea> 12 ) 13 }
Step 3 — Scroll horizontally with the orientation prop
Switch the scroll direction with the orientation prop. The default vertical applies overflow-y-auto overflow-x-hidden; passing orientation="horizontal" swaps in overflow-x-auto overflow-y-hidden so the region scrolls sideways. A horizontal rail suits a row of cards, a gallery strip, or a set of filter chips that would otherwise wrap onto a second line. Give the inner content a width greater than the container — usually a flex row with w-max and fixed-width children — so it overflows the x-axis and triggers the scroll, the same way a vertical area needs a fixed height. The orientations map is the whole switch: two keys, one per axis, read by the styles.orientations[orientation] lookup. Both axes share the same thin, themed scrollbar, so a horizontal rail looks identical to the vertical default with no extra CSS. Unlike a Carousel, this is plain native scrolling — no slides, no autoplay — which is right when the content is browse-anywhere rather than step-through.
1 <ScrollArea orientation="horizontal" className="w-full"> 2 <div className="flex gap-3 w-max pb-3"> 3 {items.map((item) => ( 4 <div 5 key={item} 6 className="h-24 w-40 shrink-0 rounded-lg border" 7 /> 8 ))} 9 </div> 10 </ScrollArea>
Step 4 — Restyle the scrollbar
Every class the component renders lives in one styles object at the top of the file you own, and the base key is where the cross-engine scrollbar work happens. For Firefox it sets [scrollbar-width:thin] and [scrollbar-color:var(--border)_transparent]; for Chrome, Edge, and Safari it stacks a set of [&::-webkit-scrollbar] rules — the bar is w-1.5 / h-1.5, the track is transparent and rounded-full, the thumb is rounded-full with bg-border, and on hover the thumb deepens to bg-muted-foreground/30. Widen the bar by bumping the w-1.5 and h-1.5 values, or tint the thumb by swapping bg-border for bg-primary/40. Because both the Firefox and WebKit paths reference the --border and --muted-foreground tokens, adjusting those in theming re-themes every scroll area for dark and light mode at once. A one-off className merges after styles.base through cn(), so a single call site can raise the height or add a border without touching the shared file. For copy-paste layouts see the Scroll Area examples.
1 // verbatim from scroll-area.tsx 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.
Frequently asked questions
No. Unlike most interactive components, the Scroll Area holds no state and calls no browser API — it is a styled <div> that maps an orientation prop to an overflow rule. Its source ships without a 'use client' directive, so it renders straight inside a Server Component in the Next.js App Router, and it works the same in any client-rendered React app. Nothing about it forces a client boundary.
A <div> only scrolls once its content overflows a fixed size, and the Scroll Area does not set one for you. For the default vertical orientation, pass a className with a height — h-48, max-h-64, or a viewport unit — so content taller than the box scrolls inside it. For a horizontal area, give the inner content a width greater than the container, such as a w-max flex row. Without the height or width constraint the div grows to fit its content and never overflows.
Pass orientation="horizontal" to the ScrollArea. The default vertical applies overflow-y-auto overflow-x-hidden; horizontal swaps in overflow-x-auto overflow-y-hidden through the orientations map in the styles object. Then make the inner content wider than the container — usually a flex row with w-max — so it overflows on the x-axis and triggers the sideways scroll. The scrollbar keeps the same thin, themed style on either axis.
Yes. The scrollbar is defined by Tailwind utilities in the styles.base object inside the file you own. Change the thumb color by editing [&::-webkit-scrollbar-thumb]:bg-border, widen the bar by bumping [&::-webkit-scrollbar]:w-1.5, and adjust the hover shade on [&:hover::-webkit-scrollbar-thumb]:bg-muted-foreground/30. Because the defaults reference the --border and --muted-foreground theme tokens, retheming those values updates every scroll area for dark and light mode at once.
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add scroll-area. The component has no dependency on Next.js or any router — it is a self-contained styled <div> with no state, no context, and no browser API, so it works anywhere React and Tailwind reach the DOM. Render it the same way, setting a height through className for a vertical area or a wider inner width for a horizontal rail.

