Scroll Area
A wrapper for Base UI ScrollArea with vertical, horizontal, and dual-axis scrollbars, gradient fade via viewportCls, keepMounted, and overflow threshold control.
Orientation
Vertical — default, scrolls up/down
Horizontal — scrolls left/right
Both — vertical + horizontal + corner
Content
Prose — long text paragraph
Code block — monospace pre content
Options
keepMounted — scrollbar persists in DOM when not needed
Fade — mask-image gradient via viewportCls
overflowEdgeThreshold — overflow data attrs fire 20px before edge
Installation
npx shadcn@latest add @glrk-ui/scroll-areaIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
'use client'
import { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area'
import { cn } from '@/lib/utils'
const ScrollAreaRoot = ScrollAreaPrimitive.Root
const ScrollAreaViewport = ScrollAreaPrimitive.Viewport
const ScrollAreaContent = ScrollAreaPrimitive.Content
const ScrollAreaThumb = ScrollAreaPrimitive.Thumb
const ScrollAreaCorner = ScrollAreaPrimitive.Corner
function ScrollArea({
className,
children,
...props
}: ScrollAreaPrimitive.Root.Props) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = 'vertical',
...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
return (
<ScrollAreaPrimitive.Scrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
'flex touch-none p-px transition-[opacity,colors] select-none opacity-0 data-[scrolling]:opacity-100 data-[hovering]:opacity-100',
'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:border-l data-[orientation=vertical]:border-l-transparent',
'data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:border-t data-[orientation=horizontal]:border-t-transparent',
className,
)}
{...props}
>
<ScrollAreaPrimitive.Thumb
data-slot="scroll-area-thumb"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.Scrollbar>
)
}
type ScrollAreaWrapperProps = ScrollAreaPrimitive.Root.Props & {
orientation?: 'vertical' | 'horizontal' | 'both'
viewportCls?: string
scrollbarCls?: string
keepMounted?: boolean
}
function ScrollAreaWrapper({
orientation = 'vertical',
viewportCls,
scrollbarCls,
keepMounted,
className,
children,
...props
}: ScrollAreaWrapperProps) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className={cn(
'size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1',
viewportCls,
)}
>
{children}
</ScrollAreaPrimitive.Viewport>
{(orientation === 'vertical' || orientation === 'both') && (
<ScrollBar orientation="vertical" keepMounted={keepMounted} className={scrollbarCls} />
)}
{(orientation === 'horizontal' || orientation === 'both') && (
<ScrollBar orientation="horizontal" keepMounted={keepMounted} className={scrollbarCls} />
)}
{orientation === 'both' && <ScrollAreaPrimitive.Corner />}
</ScrollAreaPrimitive.Root>
)
}
export {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaContent,
ScrollAreaThumb,
ScrollAreaCorner,
ScrollArea,
ScrollBar,
ScrollAreaWrapper,
}
Usage
Basic (vertical)
import { ScrollAreaWrapper } from "@/components/ui/scroll-area"
<ScrollAreaWrapper className="h-64 w-full rounded-md border">
<div className="p-4">{/* tall content */}</div>
</ScrollAreaWrapper>Horizontal
<ScrollAreaWrapper orientation="horizontal" className="w-full rounded-md border">
<div className="flex gap-4 p-4">
{items.map(item => <Card key={item.id} className="w-48 shrink-0" />)}
</div>
</ScrollAreaWrapper>Both axes
Renders vertical + horizontal scrollbars and a Corner at their intersection:
<ScrollAreaWrapper orientation="both" className="h-64 w-96 rounded-md border">
<div style={{ width: "800px" }}>
{/* wide + tall content */}
</div>
</ScrollAreaWrapper>Keep scrollbar mounted
Prevents layout shift by keeping the scrollbar in the DOM even when content fits:
<ScrollAreaWrapper keepMounted className="h-64 rounded-md border">
{/* content */}
</ScrollAreaWrapper>Gradient fade
Use the viewportCls prop to apply a mask-image gradient via the viewport element:
<ScrollAreaWrapper
className="h-64 rounded-md border"
viewportCls="[mask-image:linear-gradient(to_bottom,transparent_0,black_2rem,black_calc(100%-2rem),transparent_100%)]"
>
{/* content */}
</ScrollAreaWrapper>Overflow edge threshold
Fire overflow data attributes before reaching the exact scroll edge:
<ScrollAreaWrapper overflowEdgeThreshold={20} className="h-64 rounded-md border">
{/* content */}
</ScrollAreaWrapper>Using primitives
import {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaCorner,
ScrollBar,
} from "@/components/ui/scroll-area"
<ScrollAreaRoot className="relative h-64">
<ScrollAreaViewport className="size-full rounded-[inherit]">
{/* content */}
</ScrollAreaViewport>
<ScrollBar orientation="vertical" />
<ScrollBar orientation="horizontal" />
<ScrollAreaCorner />
</ScrollAreaRoot>Reference
ScrollAreaWrapper
Prop
Type
ScrollBar
Prop
Type