Skip to content
Drivn
6 min read

Drivn vs shadcn/ui — Scroll Area Compared

Compare Drivn Scroll Area vs shadcn/ui — a CSS-styled native scroller versus a Radix custom scrollbar. Both copy-paste, one ships zero runtime deps.

Every scrollable panel in a product — a sidebar, a chat log, a long settings list — eventually hits the same friction: the browser's default scrollbar looks nothing like the rest of the interface. Drivn's Scroll Area and the shadcn/ui component both fix that, and they disagree about how.

shadcn/ui reaches for @radix-ui/react-scroll-area. Its file exports ScrollArea and ScrollBar, hides the browser's real scrollbar behind a Radix viewport, and paints a replacement out of <div> elements whose size and offset JavaScript recomputes as content scrolls and the container resizes. Pixel parity across engines is the payoff.

Drivn takes the opposite route. @/components/ui/scroll-area.tsx is one function returning one <div>, spreading React.HTMLAttributes<HTMLDivElement>, calling no hook at all. Native scrolling stays; only the appearance of the browser's own bar changes, through scrollbar-width and scrollbar-color for Firefox plus the ::-webkit-scrollbar pseudo-elements for Chromium and Safari — every rule sitting in the styles.base string copied further down.

Because nothing in that file is client-only, it ships without a 'use client' directive and renders on the server. The sections ahead work through the dependency tree, the two scrollbar models, the orientation map, why the box needs an explicit height, and the call site each library asks you to write.

Side-by-side comparison

FeatureDrivnshadcn/ui
Underlying primitiveNative browser scrolling + CSS-styled scrollbar@radix-ui/react-scroll-area
Runtime UI dependenciescn() utility only@radix-ui/react-scroll-area
API shapeSingle component — ScrollAreaTwo exports — ScrollArea + ScrollBar
Scrollbar renderingCSS — scrollbar-width + ::-webkit-scrollbarJavaScript — Radix-painted thumb
Client componentNo — renders on the serverYes — 'use client' Radix wrapper
Orientation propvertical and horizontalvertical and horizontal
Cross-browser pixel parityBrowser-native — varies slightly by engineIdentical — Radix paints the thumb
Scroll handler at runtimeNoneJS runs on scroll + resize
LicenseMITMIT
Copy-paste install

The runtime footprint

shadcn/ui's Scroll Area is a layer over @radix-ui/react-scroll-area. The component file imports ScrollAreaPrimitive from the Radix package and re-exports ScrollAreaPrimitive.Root plus ScrollAreaPrimitive.Viewport as ScrollArea, then composes a ScrollBar from ScrollAreaPrimitive.Scrollbar, ScrollAreaPrimitive.Thumb, and ScrollAreaPrimitive.Corner. Radix supplies the JavaScript that measures the content, sizes the thumb, and keeps the custom bar in sync with the scroll position.

Drivn's Scroll Area imports React and the local cn() utility — nothing else. Every class the component uses lives in the styles object below, reproduced verbatim from the registry. styles.base sets scrollbar-width: thin and scrollbar-color for Firefox, then a run of ::-webkit-scrollbar pseudo-element rules for Chromium and Safari: a 1.5-unit-wide track, a rounded-full thumb tinted bg-border, and a hover state that darkens the thumb to bg-muted-foreground/30. No Radix, no JavaScript, no icon package. See the Scroll Area examples page for the call sites, and the Separator component for the same zero-dependency framing applied to a divider.

1// Drivn — styles object, no Radix (verbatim from registry)
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}

CSS-styled scrollbar vs Radix custom scrollbar

Here is the actual fork in the road. Drivn never draws a scrollbar; it lets the browser scroll the way it always has and restyles the bar the browser was going to paint regardless. Firefox reads scrollbar-width: thin and scrollbar-color, while Chromium and Safari read the ::-webkit-scrollbar family — a 1.5-unit track, a rounded-full thumb tinted bg-border, and a bg-muted-foreground/30 hover state. Nothing measures, nothing recalculates, and no work happens during the scroll itself.

shadcn/ui takes the opposite bet: hide the native bar, render a Radix ScrollAreaThumb, and keep its height and offset in sync with JavaScript on every scroll and resize event. What that buys is exactness — the same thumb, the same width, in every engine — plus a ScrollBar element you can restyle at will. What it costs is the Radix package, a 'use client' boundary, and a handler running while the user drags. The Drivn side of the trade is easy to audit because there is so little of it: one <div>, a cn() merge of the base styles with your className, and a ...props spread. No state means no hooks, and no hooks means the component is server-renderable by default. The Scroll Area component reference carries the full props table.

1// Drivn — the whole component is one styled <div> (verbatim)
2export function ScrollArea({
3 orientation = 'vertical',
4 className,
5 children,
6 ...props
7}: ScrollAreaProps) {
8 return (
9 <div
10 className={cn(
11 styles.base,
12 styles.orientations[orientation],
13 className
14 )}
15 {...props}
16 >
17 {children}
18 </div>
19 )
20}

The orientation prop

Both libraries expose vertical and horizontal scrolling, and both default to vertical. Drivn reads an orientation prop — 'vertical' or 'horizontal' — and uses it to pick a key from the orientations map. Vertical resolves to overflow-y-auto overflow-x-hidden, horizontal to overflow-x-auto overflow-y-hidden, so exactly one axis scrolls and the other is clipped. The map is reproduced verbatim below; there is no diagonal or both-axes mode, which keeps the contract small and predictable.

For a horizontal Scroll Area the child content has to be wider than the container — typically a flex row with w-max so the items lay out at their natural width and overflow sideways. shadcn/ui passes its orientation to the Radix ScrollBar and renders a matching horizontal Radix thumb. The end result is the same affordance; the difference stays the dependency tree and the rendering model. A horizontal Scroll Area pairs well with the Drivn Card for a swipeable row of tiles — see the Scroll Area examples for that call site.

1// Drivn — orientation picks one overflow axis (verbatim)
2orientations: {
3 vertical: 'overflow-y-auto overflow-x-hidden',
4 horizontal: 'overflow-x-auto overflow-y-hidden',
5}

Sizing — the area needs a height

A Scroll Area only scrolls when its content is taller (or wider) than the box. Neither Drivn nor shadcn/ui can infer a height for you — the component is a styled <div>, and a <div> grows to fit its content by default. To get scrolling you set a fixed height or a max-height through the className: <ScrollArea className="h-48"> caps the box at 12rem and lets anything past that scroll. This is the single most common reason a Scroll Area appears not to work — the content simply fit.

shadcn/ui has the same requirement; its Radix viewport also needs a constrained height before the custom thumb appears. Because the height lives on the className in both libraries, the sizing code is identical and a migration touches nothing here. Drivn merges that className onto the base styles through cn(), so Tailwind's height utilities, arbitrary values like h-[420px], and max-h-* classes all work. Inside a Dialog or a Card, a max-h-* on the Scroll Area keeps a long list from pushing the surrounding layout.

When each wins

Pick shadcn/ui's Scroll Area when pixel-identical scrollbars across every browser are a hard requirement — a design system that must look the same in Firefox, Chrome, and Safari down to the thumb width, or a product where the scrollbar is part of the brand. Radix's JavaScript-painted thumb gives that exact control, plus a ScrollBar element you can restyle freely. Projects already standardized on Radix pay no extra dependency cost, since the package family is likely in the bundle already.

Pick Drivn's Scroll Area when the priority is a small dependency tree and a component that renders on the server. It is one <div> with a CSS-styled native scrollbar — no Radix, no JavaScript, no 'use client' boundary, and no scroll handler running during interaction. The scrollbar still looks thin and on-theme in every modern engine; it just is not guaranteed pixel-identical between them. For most apps — a sidebar, a chat log, a horizontal card row, a long settings panel — that trade is worth it. Pair the Scroll Area with the Drivn Table for scrollable data and the Command menu for scrollable result lists. See the Scroll Area examples for ready call sites.

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

No. The Drivn Scroll Area is a plain function component — it holds no state, calls no hooks, and renders a single <div>. With no client-only API in the file, the registry source ships without a 'use client' directive, so the component renders as a server component. The shadcn/ui flavor wraps a Radix client component and therefore carries the directive. This is one of the clearest practical differences between the two.

Almost always because the area has no constrained height. A Scroll Area is a styled <div>, and a <div> grows to fit its content, so nothing overflows and nothing scrolls. Set a fixed height or a max-height through the className — for example <ScrollArea className="h-48"> or className="max-h-72". The component scrolls only once its content is taller than that box. The same rule applies to horizontal scrolling, where the content must be wider than the container.

Yes, across modern engines. Firefox honours the scrollbar-width: thin and scrollbar-color properties, while Chromium and Safari honour the ::-webkit-scrollbar pseudo-elements — the styles.base string sets both. The bar renders thin and theme-tinted everywhere, but it is not guaranteed pixel-identical between engines, since each browser paints its own scrollbar. The shadcn/ui flavor uses a Radix JavaScript-painted thumb to get exact parity instead.

Yes. Pass orientation="horizontal" and the component picks the overflow-x-auto overflow-y-hidden key from its orientations map, so the area scrolls sideways and clips the vertical axis. The child content has to be wider than the container — usually a flex row with w-max so the items keep their natural width. The default orientation is vertical, which resolves to overflow-y-auto overflow-x-hidden.

No. The component does not draw or position a scrollbar at all — it lets the browser scroll natively and restyles the browser's own scrollbar with CSS. There is no scroll handler, no thumb-sizing calculation, and no resize observer. The shadcn/ui flavor, by contrast, renders a Radix thumb whose size and offset are computed in JavaScript on every scroll and resize to keep the custom bar in sync.