Skip to content
Drivn
6 min read

Next.js Scroll Area with the App Router

Add a scroll area to a Next.js App Router app. Drivn ships a copy-paste, server-safe component that styles a native scrollbar with zero runtime dependencies.

A scroll area is the small piece of polish that keeps a fixed-height region — a sidebar, a chat log, a card of options — scrollable without the chunky native scrollbar breaking the layout. In a Next.js App Router project the practical win is that Drivn's Scroll Area needs no client boundary at all: it holds no state and calls no browser API, so scroll-area.tsx ships without a 'use client' directive and renders straight inside a Server Component.

After install the component sits in src/components/ui/scroll-area.tsx as a single function. It is a styled <div> — the styles.base object themes the scrollbar across engines ([scrollbar-width:thin] and [scrollbar-color:...] for Firefox, a stack of [&::-webkit-scrollbar] rules for Chrome and Safari), and an orientations map switches between overflow-y-auto and overflow-x-auto. You give it a height through className and it does the rest, spreading any extra div props straight through.

This guide installs Drivn in a Next.js 16 project, renders a vertical scroll region inside a Server Component, switches to a horizontal rail, and restyles the scrollbar from the one styles object you own. Every snippet comes from the component's real API. For the full reference see the Scroll Area docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Scroll Area.

Install in a Next.js 16 project

Drivn installs through a small CLI that writes component source directly into your repository — there is no runtime package to version-lock. From the root of your Next.js 16 project run npx drivn add scroll-area. The Scroll Area imports nothing past React and the local cn utility, so the CLI writes a single scroll-area.tsx file and leaves the rest of your tree untouched. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file, and future Drivn releases will not overwrite it — commit it for a clean baseline before wiring it into a layout. If the same screen renders a long option list, pair it with the Drivn Select or a Card from the same install.

1# from the root of your Next.js 16 project
2npx drivn add scroll-area

Render a scrollable region in a Server Component

Because the Scroll Area holds no state and calls no browser API, scroll-area.tsx ships without a 'use client' directive — you can render it straight inside a Server Component in a Next.js App Router page, no client boundary required. The one thing the component does not do for you is set a height: it defaults to orientation="vertical" (overflow-y-auto overflow-x-hidden), but a div only scrolls once its content overflows a fixed size, so pass a className with h-* or max-h-*. <ScrollArea className="h-48"> caps the region at twelve rem and scrolls anything taller. Any extra props — id, aria-label, onScroll — spread onto the underlying <div> through the ...props rest, so it behaves like a plain scroll container with a themed scrollbar. Drop it around a long comment thread, a tag list, or a table of contents and the content scrolls inside the box instead of pushing the page down.

1import { ScrollArea } from '@/components/ui/scroll-area'
2
3export function TermsBox() {
4 return (
5 <ScrollArea className="h-48">
6 <p>{/* long terms-of-service copy */}</p>
7 </ScrollArea>
8 )
9}

Scroll sideways 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 instead. 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 w-max — or fixed-width children in a flex row — so it exceeds the container width and triggers the horizontal 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. Because both axes share the same scrollbar theme, a horizontal rail gets the identical thin, rounded scrollbar as the vertical default without any extra CSS.

1<ScrollArea orientation="horizontal" className="w-full">
2 <div className="flex gap-3 w-max">
3 {items.map((item) => (
4 <div key={item} className="w-40 h-24 rounded-lg" />
5 ))}
6 </div>
7</ScrollArea>

How the cross-browser scrollbar styling works

Every class the component renders lives in one styles object at the top of the file, and the base key is where the cross-engine work happens. For Firefox it sets [scrollbar-width:thin] and [scrollbar-color:var(--border)_transparent], painting a thin scrollbar tinted with your theme's border token over a transparent track. 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. The [&::-webkit-scrollbar-corner] rule keeps the corner where two scrollbars meet transparent. Because both the Firefox and WebKit paths reference the same design tokens (--border, --muted-foreground), the scrollbar matches your palette in dark and light mode without a second declaration.

1// verbatim from scroll-area.tsx
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}

Restyle and theme the scrollbar

Because the scrollbar is defined entirely in the styles.base object you own, restyling it is a matter of editing Tailwind utilities in one place. Make the bar thicker by bumping [&::-webkit-scrollbar]:w-1.5 to w-2 (and the matching h-1.5); change the thumb color by swapping [&::-webkit-scrollbar-thumb]:bg-border for bg-primary/40; or soften the hover state on [&:hover::-webkit-scrollbar-thumb]:bg-muted-foreground/30. Since the defaults read theme tokens — --border for the resting thumb, --muted-foreground for hover — adjusting those tokens in theming re-themes every scroll area for dark and light mode at once, with no per-instance overrides. You can also pass a one-off className; it merges after styles.base through cn(), so a call site can raise the height, add a border, or tweak padding without touching the shared component. For copy-paste layouts and stateful patterns see the Scroll Area examples.

1// widen the bar and tint the thumb, in scroll-area.tsx
2'[&::-webkit-scrollbar]:w-2',
3'[&::-webkit-scrollbar]:h-2',
4'[&::-webkit-scrollbar-thumb]:bg-primary/40',
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

Yes. 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. Nothing about it forces a client boundary, which makes it free to drop into a server-rendered layout, sidebar, or content page.

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 simply 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. 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. In a Vite + React app there is no server boundary to think about; run npx drivn add scroll-area and render it the same way, setting a height through className for a vertical area or a wider inner width for a horizontal rail.