A focusable, named scroll region with theme-aware custom thumb and native scrolling behavior for long lists and documents.
Recent activity
Everything happening in your workspace
import { CheckCircle2, MessageSquare, UploadCloud } from "lucide-react";
import { ScrollArea } from "@/components/ballmac/scroll-area";
const entries = [
{
icon: UploadCloud,
title: "New files uploaded",
detail: "Design assets · 2 minutes ago",
},
{
icon: MessageSquare,
title: "Comment added",
detail: "Q3 product brief · 18 minutes ago",
},
{
icon: CheckCircle2,
title: "Review completed",
detail: "Release checklist · 1 hour ago",
},
{
icon: UploadCloud,
title: "Preview updated",
detail: "Launch page · 3 hours ago",
},
{
icon: MessageSquare,
title: "Feedback received",
detail: "Brand guide · Yesterday",
},
];
export default function ScrollAreaDemo() {
return (
<div className="w-full max-w-sm overflow-hidden rounded-xl border bg-card shadow-sm">
<div className="border-b px-4 py-3">
<p className="text-sm font-semibold">Recent activity</p>
<p className="text-xs text-muted-foreground">
Everything happening in your workspace
</p>
</div>
<ScrollArea label="Recent workspace activity" className="h-52">
<ul className="divide-y px-4">
{entries.map(({ icon: Icon, title, detail }) => (
<li key={title} className="flex items-start gap-3 py-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted">
<Icon aria-hidden="true" className="size-4" />
</span>
<div className="min-w-0">
<p className="text-sm font-medium">{title}</p>
<p className="text-xs text-muted-foreground">{detail}</p>
</div>
</li>
))}
</ul>
</ScrollArea>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/scroll-areaInstall the dependencies.
$ pnpm add radix-uiCopy the source into your project.
components/ballmac/scroll-area.tsx// Ballmac UI: Scroll Area. https://ui.ballmac.com/components/scroll-area "use client"; import * as React from "react"; import { ScrollArea as Primitive } from "radix-ui"; import { cn } from "@/lib/utils"; type ScrollAreaProps = React.ComponentProps<typeof Primitive.Root> & { /** Accessible name of the focusable reading region. */ label: string; }; function ScrollArea({ className, children, label, ...props }: ScrollAreaProps) { return ( <Primitive.Root data-slot="scroll-area" className={cn("relative overflow-hidden rounded-lg", className)} {...props} > <Primitive.Viewport data-slot="scroll-area-viewport" role="region" aria-label={label} tabIndex={0} className="size-full rounded-[inherit] outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50" > {children} </Primitive.Viewport> <ScrollBar /> <Primitive.Corner data-slot="scroll-area-corner" className="bg-muted" /> </Primitive.Root> ); } type ScrollBarProps = React.ComponentProps<typeof Primitive.Scrollbar>; function ScrollBar({ className, orientation = "vertical", ...props }: ScrollBarProps) { return ( <Primitive.Scrollbar data-slot="scroll-area-scrollbar" orientation={orientation} className={cn( "flex touch-none select-none p-0.5 transition-colors duration-150 motion-reduce:transition-none", orientation === "vertical" ? "h-full w-2.5 border-s border-s-transparent" : "h-2.5 flex-col border-t border-t-transparent", className, )} {...props} > <Primitive.Thumb data-slot="scroll-area-thumb" className="relative flex-1 rounded-full bg-border hover:bg-muted-foreground/50" /> </Primitive.Scrollbar> ); } export { ScrollArea, ScrollBar, type ScrollAreaProps, type ScrollBarProps };Update the import paths to match your project setup.
Usage
import { ScrollArea, ScrollBar } from "@/components/ballmac/scroll-area"The full example is in the Code tab above.
Examples
Horizontal gallery
Project stages
import { ScrollArea, ScrollBar } from "@/components/ballmac/scroll-area";
export default function ScrollAreaStates() {
return (
<div className="w-full max-w-xs">
<p className="mb-2 text-sm font-medium">Project stages</p>
<ScrollArea
label="Project stages"
type="always"
className="w-full rounded-lg border"
>
<div className="flex w-max gap-2 p-3">
{["Planning", "Design", "Build", "Review", "Launch"].map(
(stage, index) => (
<div
key={stage}
className="w-28 shrink-0 rounded-lg bg-muted p-3"
>
<p className="text-xs text-muted-foreground">0{index + 1}</p>
<p className="mt-1 text-sm font-medium">{stage}</p>
</div>
),
)}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
label*Accessible name of the focusable reading region. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Focuses the named scroll region |
| Arrow keys / Page Up / Page Down | Scrolls the focused region |
Use with AI
Constrains long content in an accessible keyboard-scrollable region without changing native scroll behavior. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Scroll Area (@ballmac/scroll-area) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Activity feeds and long lists
- Constrained side panels
Not for
- Short content that can fit naturally
- The main page scroll
Registry JSON: https://ui.ballmac.com/r/scroll-area.json
Credits
Free to use in personal and commercial projects.
- npm
- radix-ui
- Registry
- shadcn/utils
Pairs well with
Activity Feed
A compact actor and action feed with timestamps and an empty state.
App Shell
The frame of an application page: skip link, sticky header, sidebar that becomes a sheet on small screens, main area, optional aside and footer.
Aspect Ratio
A CSS-native ratio frame that reserves space for media and safely handles invalid ratio values.
Bento Grid
A responsive bento layout of feature cards that span columns and rows. Each BentoCard has a background visual slot, icon, title and description, and a link that slides up on hover or focus.