# Scroll area

> Native scrolling with a minimal scrollbar, edges that fade only where there’s more to see, and an optional peek that cuts the last item in half.

Docs: https://hextaui.com/docs/scroll-area
Markdown: https://hextaui.com/docs/scroll-area.md

```tsx title="components/examples/scroll-area/demo.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaDemo() {
  return (
    <ScrollArea peek className="h-80 w-full max-w-sm rounded-lg border">
      <ul className="flex flex-col gap-1 p-2">
        {people.map((person) => (
          <li
            key={person.id}
            className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
          >
            <span className="truncate font-medium">
              {person.id}. {person.name}
            </span>
            <span className="text-muted-foreground">{person.role}</span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/scroll-area.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/scroll-area.tsx"
"use client"

import * as React from "react"
import {
  DirectionProvider,
  useDirection,
} from "@base-ui/react/direction-provider"
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
import { cva } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    }
  }
}

type PeekItem = { top: number; height: number }

function getPeekHeight(items: PeekItem[], viewportHeight: number) {
  if (items.length === 0 || viewportHeight <= 0) {
    return null
  }

  const last = items[items.length - 1]
  if (last.top + last.height <= viewportHeight + 1) {
    return null
  }

  const crossing = items.find(
    (item) =>
      item.top < viewportHeight && item.top + item.height > viewportHeight
  )
  if (crossing && crossing.height > 0) {
    const visible = (viewportHeight - crossing.top) / crossing.height
    if (visible >= 0.4 && visible <= 0.65) {
      return null
    }
  }

  let target: { height: number; visible: number } | null = null
  for (const item of items) {
    const cut = item.top + item.height * 0.6
    if (cut <= viewportHeight) {
      target = { height: cut, visible: item.height * 0.6 }
    }
  }

  if (target === null || target.height < viewportHeight * 0.5) {
    return null
  }

  return {
    height: Math.round(target.height),
    visible: Math.round(target.visible),
  }
}

function findPeekItems(content: HTMLElement) {
  const marked = content.querySelectorAll<HTMLElement>(
    "[data-scroll-area-item]"
  )
  if (marked.length > 0) {
    return Array.from(marked)
  }
  let container: Element = content
  while (container.children.length === 1) {
    container = container.children[0]
  }
  return Array.from(container.children) as HTMLElement[]
}

function usePeek(
  enabled: boolean,
  rootRef: React.RefObject<HTMLDivElement | null>,
  viewportRef: React.RefObject<HTMLDivElement | null>,
  contentRef: React.RefObject<HTMLDivElement | null>
) {
  React.useLayoutEffect(() => {
    const root = rootRef.current
    const viewport = viewportRef.current
    const content = contentRef.current
    if (!enabled || !root || !viewport || !content) {
      return
    }

    const apply = () => {
      root.style.height = ""
      viewport.style.removeProperty("--scroll-area-fade-end")
      const viewportHeight = viewport.clientHeight
      const origin = viewport.getBoundingClientRect().top - viewport.scrollTop
      const items = findPeekItems(content).map((item) => {
        const rect = item.getBoundingClientRect()
        return { top: rect.top - origin, height: rect.height }
      })
      const target = getPeekHeight(items, viewportHeight)
      if (target === null) {
        delete root.dataset.peeking
        return
      }
      const chrome = root.offsetHeight - viewportHeight
      root.style.height = `${target.height + chrome}px`
      viewport.style.setProperty(
        "--scroll-area-fade-end",
        `${Math.max(8, Math.round(target.visible / 2))}px`
      )
      root.dataset.peeking = ""
    }

    apply()

    const observer =
      typeof ResizeObserver === "undefined" ? null : new ResizeObserver(apply)
    observer?.observe(content)
    observer?.observe(root)

    return () => {
      observer?.disconnect()
      root.style.height = ""
      viewport.style.removeProperty("--scroll-area-fade-end")
      delete root.dataset.peeking
    }
  }, [enabled, rootRef, viewportRef, contentRef])
}

function useDomDirection(rootRef: React.RefObject<HTMLDivElement | null>) {
  const contextDirection = useDirection()
  const [domDirection, setDomDirection] = React.useState<"ltr" | "rtl">("ltr")

  React.useLayoutEffect(() => {
    const root = rootRef.current
    if (root) {
      setDomDirection(
        getComputedStyle(root).direction === "rtl" ? "rtl" : "ltr"
      )
    }
  }, [rootRef])

  return domDirection === "rtl" ? "rtl" : contextDirection
}

const fadeMask =
  "[--scroll-area-fade:2.5rem] [mask-composite:intersect] [mask-image:linear-gradient(to_bottom,transparent,#000_min(var(--scroll-area-fade),var(--scroll-area-overflow-y-start,0px)),#000_calc(100%_-_min(var(--scroll-area-fade-end,var(--scroll-area-fade)),var(--scroll-area-overflow-y-end,0px))),transparent),linear-gradient(to_right,transparent,#000_min(var(--scroll-area-fade),var(--scroll-area-overflow-x-start,0px)),#000_calc(100%_-_min(var(--scroll-area-fade),var(--scroll-area-overflow-x-end,0px))),transparent)] rtl:[mask-image:linear-gradient(to_bottom,transparent,#000_min(var(--scroll-area-fade),var(--scroll-area-overflow-y-start,0px)),#000_calc(100%_-_min(var(--scroll-area-fade-end,var(--scroll-area-fade)),var(--scroll-area-overflow-y-end,0px))),transparent),linear-gradient(to_left,transparent,#000_min(var(--scroll-area-fade),var(--scroll-area-overflow-x-start,0px)),#000_calc(100%_-_min(var(--scroll-area-fade),var(--scroll-area-overflow-x-end,0px))),transparent)]"

const scrollAreaVariants = cva(
  "relative has-[>[data-slot=scroll-area-viewport]:focus-visible]:ring-3 has-[>[data-slot=scroll-area-viewport]:focus-visible]:ring-focus-ring has-[>[data-slot=scroll-area-viewport]:focus-visible]:outline-1 has-[>[data-slot=scroll-area-viewport]:focus-visible]:outline-ring has-[>[data-slot=scroll-area-viewport]:focus-visible]:outline-solid"
)

type ScrollAreaProps = ScrollAreaPrimitive.Root.Props & {
  scrollbars?: "vertical" | "horizontal" | "both"
  fade?: boolean
  peek?: boolean
  viewportRef?: React.Ref<HTMLDivElement>
}

function ScrollArea({
  className,
  children,
  scrollbars = "vertical",
  fade = true,
  peek = false,
  viewportRef,
  ref,
  ...props
}: ScrollAreaProps) {
  const rootRef = React.useRef<HTMLDivElement>(null)
  const localViewportRef = React.useRef<HTMLDivElement>(null)
  const contentRef = React.useRef<HTMLDivElement>(null)
  const setRootRef = React.useMemo(() => mergeRefs(rootRef, ref), [ref])
  const setViewportRef = React.useMemo(
    () => mergeRefs(localViewportRef, viewportRef),
    [viewportRef]
  )
  const direction = useDomDirection(rootRef)

  usePeek(peek, rootRef, localViewportRef, contentRef)

  const area = (
    <ScrollAreaPrimitive.Root
      ref={setRootRef}
      data-slot="scroll-area"
      data-peek={peek ? "" : undefined}
      className={mergeClassName(scrollAreaVariants(), className)}
      {...props}
    >
      <ScrollAreaPrimitive.Viewport
        ref={setViewportRef}
        data-slot="scroll-area-viewport"
        className={cn(
          "size-full overscroll-none rounded-[inherit] outline-none focus-visible:outline-hidden",
          fade && fadeMask
        )}
      >
        <ScrollAreaPrimitive.Content
          ref={contentRef}
          data-slot="scroll-area-content"
          className={cn(scrollbars === "vertical" && "w-full min-w-0!")}
        >
          {children}
        </ScrollAreaPrimitive.Content>
      </ScrollAreaPrimitive.Viewport>
      {scrollbars !== "horizontal" ? (
        <ScrollBar orientation="vertical" />
      ) : null}
      {scrollbars !== "vertical" ? (
        <ScrollBar orientation="horizontal" />
      ) : null}
      {scrollbars === "both" ? (
        <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
      ) : null}
    </ScrollAreaPrimitive.Root>
  )

  return <DirectionProvider direction={direction}>{area}</DirectionProvider>
}

function ScrollBar({
  className,
  orientation = "vertical",
  ...props
}: ScrollAreaPrimitive.Scrollbar.Props) {
  return (
    <ScrollAreaPrimitive.Scrollbar
      data-slot="scroll-area-scrollbar"
      orientation={orientation}
      className={mergeClassName(
        "pointer-events-none flex touch-none p-0.5 opacity-0 transition-opacity duration-200 select-none data-hovering:pointer-events-auto data-hovering:opacity-100 data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-0 data-[orientation=horizontal]:mx-1.5 data-[orientation=horizontal]:mb-0.5 data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=vertical]:my-1.5 data-[orientation=vertical]:me-0.5 data-[orientation=vertical]:w-2.5",
        className
      )}
      {...props}
    >
      <ScrollAreaPrimitive.Thumb
        data-slot="scroll-area-thumb"
        className="relative flex-1 rounded-full bg-[color-mix(in_oklab,var(--foreground)_20%,var(--background))] transition-colors duration-150 hover:bg-[color-mix(in_oklab,var(--foreground)_35%,var(--background))] active:bg-[color-mix(in_oklab,var(--foreground)_45%,var(--background))] forced-colors:bg-canvas-text"
      />
    </ScrollAreaPrimitive.Scrollbar>
  )
}

export { ScrollArea, ScrollBar, scrollAreaVariants, getPeekHeight }
export type { ScrollAreaProps, PeekItem }
```

Update the import paths to match your project setup.

## Usage

Give the scroll area a fixed height or width. Content beyond it scrolls with the native scroll behavior, and a thin scrollbar appears while you hover or scroll.

```tsx
import { ScrollArea } from "@/components/ui/scroll-area"
```

```tsx
<ScrollArea className="h-72 rounded-lg border">
  <div className="p-4">{/* long content */}</div>
</ScrollArea>
```

## Examples

### Peek

Both lists have the same height. With `peek`, on the right, the box trims itself so the last visible item is cut partway through, which shows there is more to scroll before anyone tries. It never trims the box below half its height.

```tsx title="components/examples/scroll-area/peek.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

function PeopleList() {
  return (
    <ul className="flex flex-col gap-1 p-2">
      {people.map((person) => (
        <li
          key={person.id}
          className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
        >
          <span className="truncate font-medium">
            {person.id}. {person.name}
          </span>
          <span className="text-muted-foreground">{person.role}</span>
        </li>
      ))}
    </ul>
  )
}

export function ScrollAreaPeek() {
  return (
    <div className="grid w-full max-w-xl grid-cols-2 gap-4">
      <ScrollArea className="h-80 rounded-lg border">
        <PeopleList />
      </ScrollArea>
      <ScrollArea peek className="h-80 rounded-lg border">
        <PeopleList />
      </ScrollArea>
    </div>
  )
}
```

### Text

Edges fade only where there is more content to scroll to. At the top only the bottom fades, and the fade grows in as you scroll away from an edge.

```tsx title="components/examples/scroll-area/text.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

export function ScrollAreaText() {
  return (
    <ScrollArea className="h-48 w-full max-w-md rounded-lg border">
      <div className="flex flex-col gap-3 p-4 text-sm leading-6">
        {Array.from({ length: 6 }, (_, index) => (
          <p key={index}>
            Vernacular architecture is building done outside any academic
            tradition, and without professional guidance. It reflects local
            traditions, materials and climate, and makes up most of the world’s
            built environment.
          </p>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Start at the bottom

`viewportRef` gives you the scrolling element. Set its `scrollTop` to start a chat at the latest message, so only the top edge fades.

```tsx title="components/examples/scroll-area/chat.tsx"
"use client"

import * as React from "react"

import { ScrollArea } from "@/components/ui/scroll-area"

export function ScrollAreaChat() {
  const viewportRef = React.useRef<HTMLDivElement>(null)

  React.useLayoutEffect(() => {
    const viewport = viewportRef.current
    if (viewport) {
      viewport.scrollTop = viewport.scrollHeight
    }
  }, [])

  return (
    <ScrollArea
      viewportRef={viewportRef}
      aria-label="Messages"
      className="h-56 w-full max-w-sm rounded-lg border"
    >
      <div className="flex flex-col gap-2 p-3 text-sm">
        {Array.from({ length: 24 }, (_, index) => (
          <p
            key={index}
            className={
              index % 3 === 0
                ? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
                : "self-start rounded-lg bg-muted px-3 py-2"
            }
          >
            Message {index + 1}
          </p>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Horizontal

Set `scrollbars="horizontal"` and give the content `w-max` so it can grow past the box.

```tsx title="components/examples/scroll-area/horizontal.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const tags = [
  "Design systems",
  "Motion",
  "Accessibility",
  "Typography",
  "Color",
  "Layout",
  "Forms",
  "Data tables",
  "Charts",
  "Navigation",
  "Overlays",
  "Feedback",
]

export function ScrollAreaHorizontal() {
  return (
    <ScrollArea
      scrollbars="horizontal"
      className="w-full max-w-md rounded-lg border"
    >
      <div className="flex w-max gap-2 p-3">
        {tags.map((tag) => (
          <span
            key={tag}
            className="rounded-full border px-3 py-1 text-sm whitespace-nowrap"
          >
            {tag}
          </span>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Both axes

`scrollbars="both"` shows both scrollbars and a corner where they meet, and fades all four edges.

```tsx title="components/examples/scroll-area/both.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

export function ScrollAreaBoth() {
  return (
    <ScrollArea
      scrollbars="both"
      className="h-64 w-full max-w-md rounded-lg border"
    >
      <div className="grid w-max grid-cols-[repeat(10,6rem)] gap-2 p-3">
        {Array.from({ length: 100 }, (_, index) => (
          <div
            key={index}
            className="grid aspect-square place-items-center rounded-md bg-muted text-sm"
          >
            {index + 1}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}
```

### Dynamic content

Peek and the fades update as items are added or removed. With too few items to scroll, the box keeps its height and nothing fades.

```tsx title="components/examples/scroll-area/dynamic.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"

export function ScrollAreaDynamic() {
  const [count, setCount] = React.useState(4)

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <ScrollArea peek className="h-48 rounded-lg border">
        <ul className="flex flex-col gap-1 p-2">
          {Array.from({ length: count }, (_, index) => (
            <li key={index} className="rounded-md bg-muted px-3 py-3 text-sm">
              Item {index + 1}
            </li>
          ))}
        </ul>
      </ScrollArea>
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setCount(count + 1)}>
          Add item
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => setCount(Math.max(0, count - 1))}
        >
          Remove item
        </Button>
      </div>
    </div>
  )
}
```

### On a colored surface

The fade is a mask on the content, not an overlay color, so it works on any background.

```tsx title="components/examples/scroll-area/colored-surface.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaColoredSurface() {
  return (
    <div className="w-full max-w-sm rounded-xl bg-muted p-2">
      <ScrollArea className="h-56">
        <ul className="flex flex-col gap-1 p-2">
          {people.map((person) => (
            <li
              key={person.id}
              className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-background"
            >
              <span className="truncate font-medium">
                {person.id}. {person.name}
              </span>
              <span className="text-muted-foreground">{person.role}</span>
            </li>
          ))}
        </ul>
      </ScrollArea>
    </div>
  )
}
```

### Inside a sheet

Inside a flex column, wrap it in an element with `min-h-0 flex-1` and give the scroll area `h-full` so it fills the remaining space.

```tsx title="components/examples/scroll-area/sheet.tsx"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open members
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Members</SheetTitle>
        </SheetHeader>
        <div className="min-h-0 flex-1 px-4 pb-4">
          <ScrollArea peek className="h-full">
            <ul className="flex flex-col gap-1 p-2">
              {people.map((person) => (
                <li
                  key={person.id}
                  className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
                >
                  <span className="truncate font-medium">
                    {person.id}. {person.name}
                  </span>
                  <span className="text-muted-foreground">{person.role}</span>
                </li>
              ))}
            </ul>
          </ScrollArea>
        </div>
      </SheetContent>
    </Sheet>
  )
}
```

### Without fade

Turn the edge fades off with `fade={false}`.

```tsx title="components/examples/scroll-area/without-fade.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaWithoutFade() {
  return (
    <ScrollArea fade={false} className="h-40 w-full max-w-sm rounded-lg border">
      <ul className="flex flex-col gap-1 p-2">
        {people.map((person) => (
          <li
            key={person.id}
            className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
          >
            <span className="truncate font-medium">
              {person.id}. {person.name}
            </span>
            <span className="text-muted-foreground">{person.role}</span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
```

### Right to left

Content starts at the right edge, the scrollbar and fades follow the reading direction, and the direction is picked up from the page.

```tsx title="components/examples/scroll-area/rtl.tsx"
import { ScrollArea } from "@/components/ui/scroll-area"

const tags = [
  "أنظمة التصميم",
  "الحركة",
  "إمكانية الوصول",
  "الطباعة",
  "الألوان",
  "التخطيط",
  "النماذج",
  "جداول البيانات",
  "الرسوم البيانية",
  "التنقل",
]

export function ScrollAreaRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <ScrollArea scrollbars="horizontal" className="rounded-lg border">
        <div className="flex w-max gap-2 p-3">
          {tags.map((tag) => (
            <span
              key={tag}
              className="rounded-full border px-3 py-1 text-sm whitespace-nowrap"
            >
              {tag}
            </span>
          ))}
        </div>
      </ScrollArea>
    </div>
  )
}
```

## Keyboard

The viewport joins the tab order only when it has something to scroll. Once focused, it scrolls with the browser’s native keys.

| Key | Action |
| --- | --- |
| `Tab` | Focuses the viewport when its content overflows. |
| `↑` `↓` | Scrolls vertically. |
| `←` `→` | Scrolls horizontally. |
| `Page Up` `Page Down` `Space` | Scrolls by a page. |
| `Home` `End` | Jumps to the start or end. |

## Accessibility

- Give the scroll area an `aria-label` when its purpose isn’t clear from the surrounding content, like the “Messages” label in the chat example.
- The focus ring is drawn on the scroll area itself, so it stays visible above the faded edges.

## API reference

Built on the Base UI scroll area. `ScrollArea` renders the root, viewport, content and scrollbars together.

### ScrollArea

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `scrollbars` | `"vertical" \| "horizontal" \| "both"` | `"vertical"` |  |
| `fade` | `boolean` | `true` | Fade the edges that have more content. |
| `peek` | `boolean` | `false` | Trim the height so the last visible item is cut partway. Mark items with data-scroll-area-item to choose which elements count. |
| `viewportRef` | `Ref<HTMLDivElement>` | – | The element that scrolls. |
| `overflowEdgeThreshold` | `number \| { xStart, xEnd, yStart, yEnd }` | `0` | Pixels to scroll before an edge counts as overflowing. |
| `className` | `string \| (state) => string` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="scroll-area"` | The root. |
| `data-slot="scroll-area-viewport"` | The element that scrolls. |
| `data-slot="scroll-area-content"` | Wraps your content inside the viewport. |
| `data-peek` | Present when peek is on. |
| `data-peeking` | Present while peek is trimming the height. |
| `data-scrolling` | Present while the user scrolls. |
| `data-has-overflow-x` | Present when content is wider than the viewport. |
| `data-has-overflow-y` | Present when content is taller than the viewport. |
| `data-overflow-y-start` | Present when there is more content above. Matching -y-end, -x-start and -x-end attributes exist for the other edges. |
| `--scroll-area-fade` | Size of the edge fade. Defaults to 2.5rem. |
| `--scroll-area-overflow-y-start` | Distance from the top edge in pixels. Matching -y-end, -x-start and -x-end variables exist. |

### ScrollBar

Rendered for you by `ScrollArea`. Export it only if you compose the Base UI parts yourself.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="scroll-area-scrollbar"` | The scrollbar track. |
| `data-slot="scroll-area-thumb"` | The draggable thumb. |
| `data-orientation` | vertical or horizontal. |
| `data-hovering` | Present while the pointer is over the area. |
| `data-scrolling` | Present while the user scrolls. |
| `--scroll-area-thumb-height` | The thumb’s height. |
| `--scroll-area-thumb-width` | The thumb’s width. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
