Scroll Area
A native scroll container with custom scrollbars. Built with Base UI and Tailwind CSS. Copy-paste ready.
Tags
import { ScrollArea } from "@/components/ui/scroll-area";
const tags = Array.from({ length: 50 }, (_, i) => `v1.0.0-alpha.${i}`);
export default function Particle() {
return (
<ScrollArea className="h-64 rounded-lg border">
<div className="px-4 py-2">
<h4 className="mb-2 font-medium text-sm">Tags</h4>
<div className="flex flex-col gap-1">
{tags.map((tag) => (
<div className="text-sm" key={tag}>
{tag}
</div>
))}
</div>
</div>
</ScrollArea>
);
}
Installation
pnpm dlx shadcn@latest add @coss/scroll-area
Usage
import { ScrollArea } from "@/components/ui/scroll-area"<ScrollArea className="h-64 rounded-md border">
<div className="p-4">
Just as suddenly as it had begun, the sensation stopped, leaving Alice
feeling slightly disoriented. She looked around and realized that the room
hadn't changed at all - it was she who had grown smaller, shrinking down to
a fraction of her previous size. Alice felt herself growing larger and
larger, filling up the entire room until she feared she might burst. The
sensation was both thrilling and terrifying, as if she were expanding beyond
the confines of her own body. She wondered if this was what it felt like to
be a balloon, swelling with air until it could hold no more.
</div>
</ScrollArea>Examples
Scroll Fade
Use scrollFade to mask the viewport edges so content subtly fades in and out as you scroll, hinting that more content is available without adding extra UI chrome.
Tags
import { ScrollArea } from "@/components/ui/scroll-area";
const tags = Array.from({ length: 50 }, (_, i) => `v1.0.0-alpha.${i}`);
export default function Particle() {
return (
<ScrollArea className="h-64 rounded-lg border" scrollFade>
<div className="px-4 py-2">
<h4 className="mb-2 font-medium text-sm">Tags</h4>
<div className="flex flex-col gap-1">
{tags.map((tag) => (
<div className="text-sm" key={tag}>
{tag}
</div>
))}
</div>
</div>
</ScrollArea>
);
}
Horizontal Scroll
Renders a horizontally scrollable area with a custom scrollbar for browsing wide content like tag lists or card rows.
import { ScrollArea } from "@/components/ui/scroll-area";
export default function Particle() {
return (
<ScrollArea className="max-w-96 rounded-lg border">
<div className="flex w-max gap-4 p-4">
{Array.from({ length: 20 }).map((_, i) => (
<div
className="flex h-20 w-32 shrink-0 items-center justify-center rounded-md bg-muted"
key={String(i)}
>
<span className="font-medium text-sm">Item {i + 1}</span>
</div>
))}
</div>
</ScrollArea>
);
}
Scrollbar Gutter
Enable scrollbarGutter to reserve space for the scrollbar when overflow appears, preventing layout shifts as the bar shows or hides.
import { ScrollArea } from "@/components/ui/scroll-area";
export default function Particle() {
return (
<ScrollArea className="max-w-96 rounded-lg border" scrollbarGutter>
<div className="flex w-max gap-4 p-4">
{Array.from({ length: 20 }).map((_, i) => (
<div
className="flex h-20 w-32 shrink-0 items-center justify-center rounded-md bg-muted"
key={String(i)}
>
<span className="font-medium text-sm">Item {i + 1}</span>
</div>
))}
</div>
</ScrollArea>
);
}
Chat Feed
A scrollable message thread with avatars, sender names, and timestamps — a common pattern for team chat or comment history UIs.
Hey! Are we still on for the review meeting today?
Yes, I'll be there. Just finishing up some PRs.
Perfect. I've prepared the design handoff notes.
Can we push it 30 minutes? My call ends at 2:30.
Works for me.
Sure! Updated the calendar invite.
Thanks! See you all then.
One more thing — should we demo the new dashboard too?
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { ScrollArea } from "@/components/ui/scroll-area";
const messages = [
{
id: 1,
initials: "A",
text: "Hey! Are we still on for the review meeting today?",
time: "9:41 AM",
user: "Alice",
},
{
id: 2,
initials: "B",
text: "Yes, I'll be there. Just finishing up some PRs.",
time: "9:43 AM",
user: "Bob",
},
{
id: 3,
initials: "A",
text: "Perfect. I've prepared the design handoff notes.",
time: "9:44 AM",
user: "Alice",
},
{
id: 4,
initials: "C",
text: "Can we push it 30 minutes? My call ends at 2:30.",
time: "9:47 AM",
user: "Carol",
},
{
id: 5,
initials: "B",
text: "Works for me.",
time: "9:48 AM",
user: "Bob",
},
{
id: 6,
initials: "A",
text: "Sure! Updated the calendar invite.",
time: "9:49 AM",
user: "Alice",
},
{
id: 7,
initials: "C",
text: "Thanks! See you all then.",
time: "9:50 AM",
user: "Carol",
},
{
id: 8,
initials: "B",
text: "One more thing — should we demo the new dashboard too?",
time: "9:52 AM",
user: "Bob",
},
];
export default function Particle() {
return (
<ScrollArea className="h-72 w-full max-w-sm rounded-lg border">
<div className="flex flex-col gap-4 p-4">
{messages.map(({ id, initials, text, time, user }) => (
<div className="flex items-start gap-3" key={id}>
<Avatar className="size-7 shrink-0">
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-0.5">
<div className="flex items-baseline gap-2">
<span className="font-medium text-sm">{user}</span>
<span className="text-muted-foreground text-xs">{time}</span>
</div>
<p className="text-sm">{text}</p>
</div>
</div>
))}
</div>
</ScrollArea>
);
}
Notification List
A vertically scrollable list of notifications with icon, message, timestamp, and an unread indicator dot.
Alex commented on your pull request.
2m agoSarah accepted your team invitation.
15m agoDeployment to production was successful.
1h agoJordan left a review on your design.
2h agoYour free trial expires in 3 days.
5h agoNew team member joined: Morgan.
1d agoStaging environment updated successfully.
2d agoimport {
BellIcon,
CheckCheckIcon,
MessageSquareIcon,
UserPlusIcon,
} from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
const notifications = [
{
Icon: MessageSquareIcon,
id: 1,
message: "Alex commented on your pull request.",
read: false,
time: "2m ago",
},
{
Icon: UserPlusIcon,
id: 2,
message: "Sarah accepted your team invitation.",
read: false,
time: "15m ago",
},
{
Icon: CheckCheckIcon,
id: 3,
message: "Deployment to production was successful.",
read: true,
time: "1h ago",
},
{
Icon: MessageSquareIcon,
id: 4,
message: "Jordan left a review on your design.",
read: true,
time: "2h ago",
},
{
Icon: BellIcon,
id: 5,
message: "Your free trial expires in 3 days.",
read: true,
time: "5h ago",
},
{
Icon: UserPlusIcon,
id: 6,
message: "New team member joined: Morgan.",
read: true,
time: "1d ago",
},
{
Icon: CheckCheckIcon,
id: 7,
message: "Staging environment updated successfully.",
read: true,
time: "2d ago",
},
];
export default function Particle() {
return (
<ScrollArea className="h-72 w-full max-w-sm rounded-lg border">
<div className="flex flex-col">
{notifications.map(({ Icon, id, message, read, time }) => (
<div
className={`flex items-start gap-3 border-b px-4 py-3 last:border-b-0 ${!read ? "bg-accent/30" : ""}`}
key={id}
>
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-muted">
<Icon aria-hidden="true" className="size-4" />
</div>
<div className="flex flex-1 flex-col gap-0.5">
<p className="text-sm">{message}</p>
<span className="text-muted-foreground text-xs">{time}</span>
</div>
{!read && (
<span
aria-label="Unread"
className="mt-2 size-2 shrink-0 rounded-full bg-primary"
/>
)}
</div>
))}
</div>
</ScrollArea>
);
}
Sidebar Navigation
A scrollable sidebar with section labels and icon-prefixed nav items — useful for settings panels or admin layouts.
Workspace
Settings
Resources
import {
BellIcon,
FolderIcon,
LayoutDashboardIcon,
SettingsIcon,
ShieldIcon,
UsersIcon,
} from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
const sections = [
{
items: [
{ Icon: LayoutDashboardIcon, label: "Overview" },
{ Icon: FolderIcon, label: "Projects" },
{ Icon: UsersIcon, label: "Team" },
],
title: "Workspace",
},
{
items: [
{ Icon: SettingsIcon, label: "General" },
{ Icon: ShieldIcon, label: "Security" },
{ Icon: UsersIcon, label: "Members" },
],
title: "Settings",
},
{
items: [
{ Icon: FolderIcon, label: "Assets" },
{ Icon: LayoutDashboardIcon, label: "Analytics" },
{ Icon: BellIcon, label: "Alerts" },
],
title: "Resources",
},
];
export default function Particle() {
return (
<ScrollArea className="h-72 w-48 rounded-lg border">
<div className="flex flex-col gap-4 p-3">
{sections.map(({ items, title }) => (
<div key={title}>
<p className="mb-1 px-2 font-medium text-muted-foreground text-xs uppercase tracking-wide">
{title}
</p>
<div className="flex flex-col gap-0.5">
{items.map(({ Icon, label }, idx) => (
<button
className={`flex items-center gap-2.5 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent ${title === "Workspace" && idx === 0 ? "bg-accent" : ""}`}
key={label}
type="button"
>
<Icon aria-hidden="true" className="size-4 opacity-70" />
{label}
</button>
))}
</div>
</div>
))}
</div>
</ScrollArea>
);
}
Activity Log
A scrollFade-less monospace log viewer with colored level labels (info, warn, error) and per-entry timestamps.
import { ScrollArea } from "@/components/ui/scroll-area";
const logs = [
{
id: 1,
level: "info",
message: "Server started on port 3000",
time: "10:00:01",
},
{
id: 2,
level: "info",
message: "Connected to database successfully",
time: "10:00:02",
},
{
id: 3,
level: "warn",
message: "Deprecated API endpoint accessed: /api/v1/users",
time: "10:00:15",
},
{
id: 4,
level: "info",
message: "GET /api/health → 200 (12ms)",
time: "10:00:23",
},
{
id: 5,
level: "error",
message: "Failed to send email: SMTP connection refused",
time: "10:00:31",
},
{
id: 6,
level: "info",
message: "Cache cleared for key: user:sessions",
time: "10:00:44",
},
{
id: 7,
level: "warn",
message: "Rate limit threshold reached for IP 192.168.1.42",
time: "10:01:02",
},
{
id: 8,
level: "info",
message: "POST /api/auth/login → 200 (38ms)",
time: "10:01:09",
},
{
id: 9,
level: "error",
message: "Unhandled promise rejection in worker thread",
time: "10:01:22",
},
{
id: 10,
level: "info",
message: "Scheduled job 'cleanup' completed in 142ms",
time: "10:01:30",
},
];
const levelClass: Record<string, string> = {
error: "text-red-500",
info: "text-muted-foreground",
warn: "text-yellow-500",
};
export default function Particle() {
return (
<ScrollArea className="h-64 w-full max-w-xl rounded-lg border bg-muted/30">
<div className="flex flex-col gap-0.5 p-3 font-mono text-xs">
{logs.map(({ id, level, message, time }) => (
<div className="flex items-start gap-3" key={id}>
<span className="shrink-0 text-muted-foreground">{time}</span>
<span
className={`w-10 shrink-0 font-semibold uppercase ${levelClass[level]}`}
>
{level}
</span>
<span
className={
levelClass[level] !== "text-muted-foreground"
? levelClass[level]
: ""
}
>
{message}
</span>
</div>
))}
</div>
</ScrollArea>
);
}
File Explorer
A scrollFade-enabled file tree with folder and file icons, truncated names, and hover highlight on each row.
import { FileIcon, FolderIcon } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
const files = [
{ isFolder: true, name: "app", path: "app/" },
{ isFolder: true, name: "components", path: "app/components/" },
{ isFolder: false, name: "layout.tsx", path: "app/layout.tsx" },
{ isFolder: false, name: "page.tsx", path: "app/page.tsx" },
{ isFolder: false, name: "globals.css", path: "app/globals.css" },
{ isFolder: true, name: "lib", path: "lib/" },
{ isFolder: false, name: "utils.ts", path: "lib/utils.ts" },
{ isFolder: false, name: "auth.ts", path: "lib/auth.ts" },
{ isFolder: true, name: "public", path: "public/" },
{ isFolder: false, name: "favicon.ico", path: "public/favicon.ico" },
{ isFolder: false, name: "logo.svg", path: "public/logo.svg" },
{ isFolder: false, name: "next.config.ts", path: "next.config.ts" },
{ isFolder: false, name: "tailwind.config.ts", path: "tailwind.config.ts" },
{ isFolder: false, name: "package.json", path: "package.json" },
{ isFolder: false, name: "tsconfig.json", path: "tsconfig.json" },
];
export default function Particle() {
return (
<ScrollArea className="h-64 w-56 rounded-lg border" scrollFade>
<div className="flex flex-col gap-0.5 p-2">
{files.map(({ isFolder, name, path }) => {
const Icon = isFolder ? FolderIcon : FileIcon;
return (
<button
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm transition-colors hover:bg-accent"
key={path}
type="button"
>
<Icon aria-hidden="true" className="size-4 shrink-0 opacity-60" />
<span className="truncate">{name}</span>
</button>
);
})}
</div>
</ScrollArea>
);
}

