# Shadcn Scroll Area

> 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.

Source: https://designrevision.com/components/scroll-area

The **Shadcn Scroll Area** is a **custom scrollbar** for React — it replaces the native browser scrollbar with one that looks identical on macOS, Windows, and Linux, while keeping real scrolling, keyboard support, and touch. Built on <a href="https://www.radix-ui.com/primitives/docs/components/scroll-area" rel="nofollow">Radix</a>, it wraps any overflowing content and renders a consistent, styleable thumb. The examples below cover a vertical list, a horizontal row, a notification feed, and long text.

## The shape

```tsx
<ScrollArea className="h-72 w-48 rounded-md border">
  {/* long content */}
</ScrollArea>
```

Give the `ScrollArea` a **fixed height** (or max-height) so the content can overflow — that bounded size is what makes it scroll. The styled scrollbar appears only when there's overflow.

## Why not `overflow: auto`?

Native scrollbars differ across operating systems and overlay scrollbars hide until you scroll, which makes overflow hard to discover. `ScrollArea` renders a consistent thumb in every browser, so a scrolling region looks the same everywhere and stays on-brand — while still using native scrolling underneath for performance and accessibility.

## Horizontal scrolling

The default scrollbar is vertical. For a sideways-scrolling row, lay the content out in a row and add a horizontal `ScrollBar`:

```tsx
<ScrollArea className="w-96 whitespace-nowrap rounded-md border">
  <div className="flex gap-4 p-4">{/* cards */}</div>
  <ScrollBar orientation="horizontal" />
</ScrollArea>
```

## Styling the scrollbar

Because the thumb is a real element — not the browser chrome — every property is yours. Render `ScrollBar` yourself and style its width, colour, and radius with Tailwind, or rely on the default that `ScrollArea` includes.

## Common patterns

- **Notification panel** — a fixed-height activity feed (the Notification list example).
- **Terms / long prose** — a scrollable block of legal text in a dialog (the Long text example).
- **Card rail** — a horizontal row of items that scrolls sideways (the Horizontal example).

## Accessibility

`ScrollArea` uses native scrolling underneath, so the wheel, trackpad, touch dragging, and keyboard (arrows, Page Up / Down) all behave as users expect. The custom thumb is purely visual — the scrolling mechanics stay native, which is what keeps it accessible.

## Installation

### scroll-area

**Install:**

```bash
npx shadcn@latest add @designrevision/scroll-area
```

**Dependencies:** @radix-ui/react-scroll-area, utils

**Usage & accessibility:** A custom scrollbar / scroll area built on Radix. Wrap content in <ScrollArea className="h-[…]"> and it renders a styled, cross-browser scrollbar over a native-scrolling viewport. For horizontal scrolling add a <ScrollBar orientation="horizontal" /> inside. Give the ScrollArea a fixed height (or max-height) or there's nothing to scroll.

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/scroll-area-demo-01
```

**Dependencies:** scroll-area, separator

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/scroll-area-horizontal-01
```

**Dependencies:** scroll-area

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/scroll-area-notifications-01
```

**Dependencies:** scroll-area

```tsx
// 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.

**Install:**

```bash
npx shadcn@latest add @designrevision/scroll-area-text-01
```

**Dependencies:** scroll-area

```tsx
// 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>
  )
}
```
