Skip to content
gnosticaignostic-ui
Esc
↑↓navigate↵open⌘Jpreview
On this page

Scroll Area

Native scrolling for Astro panels, with custom tracks, keyboard access, and optional edge fades.

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

<ScrollArea
  scrollFade
  aria-label="Project files"
  class="max-h-64 w-72 rounded-lg border"
>
  <ul class="divide-border divide-y px-4">
    {Array.from({ length: 16 }, (_, index) => (
      <li class="py-3 text-sm">Project file {index + 1}</li>
    ))}
  </ul>
</ScrollArea>
Open preview in a new tab

Installation

bunx --bun shadcn@latest add @gnosticai/scroll-area

Usage

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

<ScrollArea aria-label="Project files" class="max-h-64 rounded-md border">
  <div class="p-4">Your content</div>
</ScrollArea>

The component uses 6px tracks, rounded thumbs at 20% foreground opacity, and scrollbars that appear on hover, keyboard focus, or scrolling. The focus ring uses the current theme.

Astro renders the component parts. A small client script measures overflow and handles thumb dragging and track clicks. The viewport uses native wheel, touch, and keyboard scrolling. Browser scrollbars remain available when JavaScript is off.

Component parts

ScrollArea assembles the parts. Use ScrollAreaRoot when you need direct control:

---
import {
  ScrollAreaRoot,
  ScrollAreaViewport,
  ScrollAreaContent,
  ScrollAreaScrollbar,
  ScrollAreaThumb,
  ScrollAreaCorner,
} from "@/components/ui/scroll-area"
---

<ScrollAreaRoot orientation="both" class="h-64 w-80 rounded-md border">
  <ScrollAreaViewport aria-label="Project files">
    <ScrollAreaContent class="p-4">Your content</ScrollAreaContent>
  </ScrollAreaViewport>
  <ScrollAreaScrollbar><ScrollAreaThumb /></ScrollAreaScrollbar>
  <ScrollAreaScrollbar orientation="horizontal"><ScrollAreaThumb /></ScrollAreaScrollbar>
  <ScrollAreaCorner />
</ScrollAreaRoot>

Keep the viewport, scrollbars, and corner as direct children of the root. Keep the content as a direct child of the viewport. Each scrollbar contains one thumb. ScrollAreaScrollbar supplies a thumb when its default slot is empty. ScrollBar is an alias for ScrollAreaScrollbar.

---
import {
  ScrollAreaRoot,
  ScrollAreaViewport,
  ScrollAreaContent,
  ScrollAreaScrollbar,
  ScrollAreaThumb,
  ScrollAreaCorner,
} from "@gnosticai/ui/ui/scroll-area"
---

<ScrollAreaRoot orientation="both" class="h-56 w-80 rounded-md border">
  <ScrollAreaViewport aria-label="Custom scroll area">
    <ScrollAreaContent class="p-4">
      <div class="flex w-max flex-col gap-3 text-sm">
        {Array.from({ length: 16 }, (_, i) => (
          <p>Row {i + 1}: This longer line can scroll in both directions.</p>
        ))}
      </div>
    </ScrollAreaContent>
  </ScrollAreaViewport>
  <ScrollAreaScrollbar>
    <ScrollAreaThumb class="bg-primary/40" />
  </ScrollAreaScrollbar>
  <ScrollAreaScrollbar orientation="horizontal">
    <ScrollAreaThumb class="bg-primary/40" />
  </ScrollAreaScrollbar>
  <ScrollAreaCorner />
</ScrollAreaRoot>
Open preview in a new tab

Examples

Scroll fade

Set scrollFade to fade an edge only when there is more content beyond it. Change --scroll-area-fade-size to adjust the fade length. Its default is 1.5rem.

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

<ScrollArea
  scrollFade
  class="max-h-64 w-72 rounded-md border"
  aria-label="Activity"
>
  <ul class="flex flex-col gap-4 p-4 text-sm">
    {Array.from({ length: 16 }, (_, i) => (
      <li>Activity {i + 1}: The project is ready for review.</li>
    ))}
  </ul>
</ScrollArea>
Open preview in a new tab

Horizontal scrolling

Use orientation="horizontal" with content wider than the viewport.

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

<ScrollArea
  orientation="horizontal"
  aria-label="Project stages"
  class="w-80 rounded-lg border"
>
  <ol class="flex w-max gap-4 p-4">
    {["Plan", "Design", "Build", "Review", "Release"].map((stage, index) => (
      <li class="bg-muted flex w-36 shrink-0 flex-col gap-2 rounded-md p-4">
        <span class="text-muted-foreground text-sm">Step {index + 1}</span>
        <span class="font-medium">{stage}</span>
      </li>
    ))}
  </ol>
</ScrollArea>
Open preview in a new tab

Both scrollbars

Use orientation="both". The corner keeps the two tracks apart.

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

<ScrollArea
  orientation="both"
  scrollbarGutter
  class="h-64 w-80 rounded-md border"
  aria-label="Grid of cells"
>
  <div class="grid w-max grid-cols-8 gap-2 p-4">
    {Array.from({ length: 80 }, (_, i) => (
      <div class="bg-muted flex size-16 items-center justify-center rounded-md text-sm">
        {i + 1}
      </div>
    ))}
  </div>
</ScrollArea>
Open preview in a new tab

Scrollbar space and containment

scrollbarGutter reserves space inside the viewport for each enabled axis. The space remains when content is short, so the content width does not change when scrollbars appear. overscrollContain stops scrolling from passing to a parent scroller when the viewport reaches an edge. It applies only on overflowing axes.

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

<ScrollArea
  scrollbarGutter
  overscrollContain
  class="max-h-64 w-72 rounded-md border"
  aria-label="Notifications"
>
  <ul class="flex flex-col gap-4 p-4 text-sm">
    {Array.from({ length: 12 }, (_, i) => (
      <li>Notification {i + 1}: A new file is available.</li>
    ))}
  </ul>
</ScrollArea>
Open preview in a new tab

Fill the viewport

Use fill for a flex column with a footer at the bottom. Keep its default value for lists whose height follows their content.

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

<ScrollArea
  fill
  class="h-64 w-72 rounded-md border"
  aria-label="Project summary"
>
  <div class="flex h-full flex-col gap-4 p-4 text-sm">
    <p>Current project</p>
    <p class="text-muted-foreground">The content fills the viewport.</p>
    <footer class="text-muted-foreground mt-auto">Last updated today</footer>
  </div>
</ScrollArea>
Open preview in a new tab

Set the height

Put size classes on ScrollArea or ScrollAreaRoot. Use max-h-64 to let short content keep its natural height and scroll when it exceeds 16rem. Use h-64 when the container must always be 16rem high. Put content padding on a child.

Inherit a maximum height

The root uses max-height: inherit by default. This works when its direct parent has a maximum height such as max-h-48. The viewport fits inside the root.

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

<div class="max-h-48 w-72 rounded-lg border">
  <ScrollArea aria-label="Recent updates">
    <ul class="flex flex-col gap-3 p-4 text-sm">
      {Array.from({ length: 12 }, (_, index) => (
        <li>Update {index + 1}: Content is ready for review.</li>
      ))}
    </ul>
  </ScrollArea>
</div>
Open preview in a new tab

inherit copies the parent’s computed max-height. It does not copy height, search other ancestors, or calculate space below a header. If the parent has no maximum height, the inherited value is none. A percentage still needs a definite containing block height. Parent padding and borders can reduce the available space. Use max-h-none to remove an inherited limit.

See MDN’s max-height reference.

Use the remaining panel space

Give the parent a height and flex flex-col. Set flex-1 on ScrollArea to use space below a header. The root and viewport have min-h-0, so they can shrink below their content height. Intermediate flex or grid items can also need min-h-0.

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

<section class="flex h-64 w-72 flex-col rounded-lg border">
  <h3 id="scroll-area-tasks" class="shrink-0 border-b p-4 font-medium">
    Tasks
  </h3>
  <ScrollArea aria-labelledby="scroll-area-tasks" class="flex-1">
    <ul class="divide-border divide-y px-4">
      {Array.from({ length: 16 }, (_, index) => (
        <li class="py-3 text-sm">Review task {index + 1}</li>
      ))}
    </ul>
  </ScrollArea>
</section>
Open preview in a new tab

Keyboard access

Give ScrollArea an aria-label or aria-labelledby. These attributes, role, aria-describedby, and tabindex pass to the viewport. In a manual composition, put them on ScrollAreaViewport. A named viewport has role="region" by default. Focus the viewport to use native arrow keys, Page Up, and Page Down. Custom tracks are hidden from assistive technology; the viewport supplies keyboard access.

API Reference

ScrollArea and ScrollAreaRoot accept these options:

PropType
orientation?'vertical' | 'horizontal' | 'both'

Allowed scroll directions.

Type'vertical' | 'horizontal' | 'both'
Default'vertical'
scrollFade?boolean

Fade edges that have more content.

Typeboolean
Defaultfalse
scrollbarGutter?boolean

Reserve space for the enabled scrollbars.

Typeboolean
Defaultfalse
fill?boolean

Size the content wrapper to fill the viewport.

Typeboolean
Defaultfalse
clampContentMinWidth?boolean

Allow the content wrapper to shrink to the viewport width.

Typeboolean
Defaulttrue
overscrollContain?boolean

Contain scroll input on overflowing axes.

Typeboolean
Defaultfalse

ScrollArea also accepts viewportProps for native viewport attributes and classes. Other native attributes and class apply to the root. Each part accepts native div attributes and a default slot, except the empty corner. The root renders a custom HTML element. All parts use Astro class and slots.

ScrollAreaScrollbar accepts orientation="vertical" (default) or "horizontal". ScrollAreaViewport has tabindex="0" by default. The content wrapper uses min-width: 0 unless clampContentMinWidth={false} is set. Horizontal content should declare its width, for example with w-max.

Read the installed source for the component source.

When to use Scroll Area

Use a Scroll Area when a bounded list or panel must scroll independently of the page. Set its height or maximum height and put padding inside its content. Without a size limit, the container can grow with the content and has no reason to scroll. For horizontal content, give the child a width such as w-max.

Keyboard and behavior

The custom element keeps a native scroll viewport. Wheel, touch, and keyboard scrolling use the browser. Its script measures overflow and moves the custom thumbs; it does not replace the viewport with a simulated scroller. Give the viewport a useful name and keep its focus outline visible. With scripts off, the browser scrollbar remains available.

Theme and layout

Track and thumb colors follow the foreground token. Check contrast over tinted surfaces and check both axes after a theme change. scrollFade is an optional edge mask, not a loading signal. overscrollContain is useful in a fixed panel but can change how a user reaches the page beneath it. Content is not virtualized, so large lists still create all their HTML nodes.

Use Table for tabular data and Dialog for a modal task. Keep one clear scroll region in each panel when possible. For most prose pages, ordinary page scrolling is easier to use.

Was this page helpful?