ScrollArea
LayoutKeyboard-focusable scrolling with vertical and horizontal tracks.
Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.
"use client";
import { ScrollArea } from "@/components/ui/scroll-area";
export default function ScrollAreaExample() {
return (
<div className="w-full max-w-sm space-y-5">
<ScrollArea
viewportLabel="Team members"
className="h-48 rounded-xl border"
>
<div className="divide-y px-4">
{Array.from({ length: 16 }, (_, index) => (
<div key={index} className="py-3 text-sm">
Team member {index + 1}
<span className="ms-2 text-xs text-muted-foreground">
Product team
</span>
</div>
))}
</div>
</ScrollArea>
<ScrollArea
orientation="horizontal"
viewportLabel="Recent projects"
className="w-full rounded-xl border"
>
<div className="flex w-max gap-3 p-4">
{[
"Brand refresh",
"Customer portal",
"Design system",
"Mobile app",
].map((name) => (
<div
key={name}
className="w-40 rounded-lg border bg-muted/30 p-4 text-sm"
>
{name}
</div>
))}
</div>
</ScrollArea>
</div>
);
}Usage
Give the root a height for vertical scrolling or a bounded width for horizontal scrolling. Set orientation to vertical, horizontal, or both. Native scrolling remains available inside the Radix viewport.
import { ScrollArea } from "@/components/ui/scroll-area";Shipped as editable source in src/components/ui/scroll-area.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.
Component source
The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.
src/components/ui/scroll-area.tsx
"use client";
import { useDocumentDirection } from "@/lib/use-document-direction";
import type { ComponentProps } from "react";
import { cn } from "@dashboardpack/core/lib/utils";
import { ScrollArea as Primitive } from "radix-ui";
export function ScrollArea({
className,
children,
orientation = "vertical",
viewportLabel = "Scrollable content",
...props
}: ComponentProps<typeof Primitive.Root> & {
orientation?: "vertical" | "horizontal" | "both";
viewportLabel?: string;
}) {
const dir = useDocumentDirection();
return (
<Primitive.Root
dir={dir}
className={cn("relative overflow-hidden", className)}
{...props}
>
<Primitive.Viewport
tabIndex={0}
role="region"
aria-label={viewportLabel}
className="size-full rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
>
{children}
</Primitive.Viewport>
{orientation !== "horizontal" && <ScrollBar orientation="vertical" />}
{orientation !== "vertical" && <ScrollBar orientation="horizontal" />}
<Primitive.Corner />
</Primitive.Root>
);
}
function ScrollBar({
orientation,
}: {
orientation: "vertical" | "horizontal";
}) {
return (
<Primitive.Scrollbar
orientation={orientation}
className={cn(
"flex touch-none select-none p-0.5",
orientation === "vertical"
? "h-full w-2 border-s border-s-transparent"
: "h-2 flex-col border-t border-t-transparent",
)}
>
<Primitive.Thumb className="relative flex-1 rounded-full bg-muted-foreground/40" />
</Primitive.Scrollbar>
);
}Required helper: src/lib/use-document-direction.ts
Copy this helper alongside the component. It keeps keyboard navigation aligned with the page direction and supports live RTL changes.
"use client";
import { useSyncExternalStore } from "react";
function subscribe(notify: () => void) {
const observer = new MutationObserver(notify);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["dir"],
});
return () => observer.disconnect();
}
function snapshot(): "ltr" | "rtl" {
return document.documentElement.dir === "rtl" ? "rtl" : "ltr";
}
/** Keep headless controls aligned with the document, including live RTL changes. */
export function useDocumentDirection() {
return useSyncExternalStore(subscribe, snapshot, () => "ltr" as const);
}API reference
Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.
| Prop | Type / Default | Description |
|---|---|---|
| orientation | vertical | horizontal | bothDefault: vertical | Tracks to render. |
| viewportLabel | stringDefault: Scrollable content | Accessible viewport region name. |
| type | auto | always | scroll | hoverDefault: hover | Radix scrollbar visibility policy. |
| className | stringDefault: — | Set root bounds such as h-48. |