Free shadcn/ui scroll area for React — a custom scrollbar built on Radix that looks consistent across browsers: a vertical list, a horizontal row, a notification feed, and long text.

Installation

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-scroll-area
utils

Props

No props documented yet.

Copied!
components/ui/scroll-area.tsx
"use client"

import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"

import { cn } from "@/lib/utils"

function ScrollArea({
  className,
  children,
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
  return (
    <ScrollAreaPrimitive.Root
      data-slot="scroll-area"
      className={cn("relative", className)}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        data-slot="scroll-area-viewport"
        className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
      >
        {children}
      </ScrollAreaPrimitive.Viewport>
      <ScrollBar />
      <ScrollAreaPrimitive.Corner />
    </ScrollAreaPrimitive.Root>
  )
}

function ScrollBar({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
  return (
    <ScrollAreaPrimitive.ScrollAreaScrollbar
      data-slot="scroll-area-scrollbar"
      orientation={orientation}
      className={cn(
        "flex touch-none p-px transition-colors select-none",
        orientation === "vertical" &&
          "h-full w-2.5 border-l border-l-transparent",
        orientation === "horizontal" &&
          "h-2.5 flex-col border-t border-t-transparent",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.ScrollAreaThumb
        data-slot="scroll-area-thumb"
        className="bg-border relative flex-1 rounded-full"
      />
    </ScrollAreaPrimitive.ScrollAreaScrollbar>
  )
}

export { ScrollArea, ScrollBar }

Examples

Basic

The canonical vertical scroll area — a tag list with a styled scrollbar.

Packages

scroll-area
separator

Props

No props documented yet.

Copied!
components/ui/scroll-area-demo-01.tsx
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"

const tags = Array.from({ length: 40 }).map(
  (_, i, a) => `v1.2.0-beta.${a.length - i}`
)

export default function ScrollAreaDemo01() {
  return (
    <ScrollArea className="h-72 w-48 rounded-md border">
      <div className="p-4">
        <h4 className="mb-4 text-sm leading-none font-medium">Tags</h4>
        {tags.map((tag) => (
          <div key={tag}>
            <div className="text-sm">{tag}</div>
            <Separator className="my-2" />
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}

Horizontal

A horizontally-scrolling row of cards with a horizontal scrollbar.

Packages

scroll-area

Props

No props documented yet.

Copied!
components/ui/scroll-area-horizontal-01.tsx
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"

const works = [
  { id: 1, artist: "Ornella Binni", gradient: "from-rose-400 to-orange-300" },
  { id: 2, artist: "Tom Byrom", gradient: "from-sky-400 to-indigo-300" },
  { id: 3, artist: "Vladimir Malyavko", gradient: "from-emerald-400 to-teal-300" },
  { id: 4, artist: "Maya Reyes", gradient: "from-fuchsia-400 to-pink-300" },
  { id: 5, artist: "Leo Park", gradient: "from-amber-400 to-yellow-300" },
  { id: 6, artist: "Nia Cole", gradient: "from-violet-400 to-purple-300" },
]

export default function ScrollAreaHorizontal01() {
  return (
    <ScrollArea className="w-full max-w-md rounded-md border whitespace-nowrap">
      <div className="flex w-max gap-4 p-4">
        {works.map((work) => (
          <figure key={work.id} className="shrink-0">
            <div
              className={`h-40 w-32 rounded-md bg-gradient-to-b ${work.gradient}`}
            />
            <figcaption className="text-muted-foreground pt-2 text-xs">
              Photo by{" "}
              <span className="text-foreground font-medium">
                {work.artist}
              </span>
            </figcaption>
          </figure>
        ))}
      </div>
      <ScrollBar orientation="horizontal" />
    </ScrollArea>
  )
}

Notification list

A scrollable activity feed — the notification-panel pattern.

Packages

scroll-area

Props

No props documented yet.

Copied!
components/ui/scroll-area-notifications-01.tsx
import { ScrollArea } from "@/components/ui/scroll-area"

const notifications = Array.from({ length: 12 }).map((_, i) => ({
  id: i,
  title: [
    "Your call has been confirmed.",
    "You have a new message.",
    "Your subscription is expiring soon.",
    "A new device signed in.",
  ][i % 4],
  time: `${(i + 1) * 5} min ago`,
}))

export default function ScrollAreaNotifications01() {
  return (
    <ScrollArea className="h-72 w-full max-w-sm rounded-md border">
      <div className="p-2">
        {notifications.map((n) => (
          <div
            key={n.id}
            className="hover:bg-accent flex items-start gap-3 rounded-md p-2"
          >
            <span className="bg-primary mt-1.5 size-2 shrink-0 rounded-full" />
            <div className="space-y-0.5">
              <p className="text-sm leading-none font-medium">{n.title}</p>
              <p className="text-muted-foreground text-xs">{n.time}</p>
            </div>
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}

Long text

A fixed-height scroll area for long prose like terms of service.

Packages

scroll-area

Props

No props documented yet.

Copied!
components/ui/scroll-area-text-01.tsx
import { ScrollArea } from "@/components/ui/scroll-area"

export default function ScrollAreaText01() {
  return (
    <ScrollArea className="h-64 w-full max-w-sm rounded-md border p-4">
      <h4 className="mb-2 text-sm font-medium">Terms of Service</h4>
      <div className="text-muted-foreground space-y-3 text-sm leading-relaxed">
        <p>
          By accessing or using this service, you agree to be bound by these
          terms. If you do not agree to all of the terms, you may not use the
          service.
        </p>
        <p>
          You are responsible for safeguarding the credentials you use to
          access the service and for any activities or actions under your
          account.
        </p>
        <p>
          The service and its original content, features, and functionality are
          and will remain the exclusive property of the company and its
          licensors.
        </p>
        <p>
          We may terminate or suspend access to the service immediately, without
          prior notice or liability, for any reason, including a breach of these
          terms.
        </p>
        <p>
          These terms shall be governed and construed in accordance with the
          laws of the jurisdiction in which the company is established, without
          regard to its conflict-of-law provisions.
        </p>
        <p>
          We reserve the right to modify or replace these terms at any time. By
          continuing to access the service after revisions become effective, you
          agree to be bound by the revised terms.
        </p>
      </div>
    </ScrollArea>
  )
}

Frequently asked questions

It is a custom scrollbar built on @radix-ui/react-scroll-area. It replaces the native browser scrollbar with one that looks identical across browsers and operating systems, while keeping real scrolling, keyboard support, and touch behaviour. You wrap any overflowing content in ScrollArea and give it a fixed height.
Native scrollbars look different on macOS, Windows, and Linux, and overlay scrollbars hide until you scroll — which makes overflow hard to discover. ScrollArea renders a consistent, always-styleable thumb in every browser, so a scrolling region looks the same everywhere and stays on-brand. Under the hood it still uses native scrolling for performance and accessibility.
Lay the content out in a row (flex with whitespace-nowrap) and add a horizontal scrollbar:
. The default scrollbar is vertical, so add the horizontal ScrollBar explicitly when the content overflows sideways.
A ScrollArea only scrolls when it has a bounded size. Give the ScrollArea (or its viewport) a fixed height or max-height — e.g. className="h-72" — so the content can overflow it. Without a height constraint there is nothing to scroll past.
The thumb lives in the ScrollBar part, so style it there with Tailwind classes (width, colour, radius). You can render ScrollBar yourself to customise it, or rely on the default that ScrollArea includes. Because it is a real element — not the browser chrome — every property is yours to set.
Yes. ScrollArea uses native scrolling underneath, so the mouse wheel, trackpad, touch dragging, and keyboard (arrows, Page Up / Down) all work as users expect. The custom thumb is purely visual; the scrolling mechanics stay native, which is what keeps it accessible.