Glrk UI

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

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/scroll-area.tsx
'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