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>
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>
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>
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>
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>
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>
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>
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>
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>
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:
orientation?'vertical' | 'horizontal' | 'both'
Allowed scroll directions.
'vertical' | 'horizontal' | 'both''vertical'scrollFade?boolean
Fade edges that have more content.
booleanfalsescrollbarGutter?boolean
Reserve space for the enabled scrollbars.
booleanfalsefill?boolean
Size the content wrapper to fill the viewport.
booleanfalseclampContentMinWidth?boolean
Allow the content wrapper to shrink to the viewport width.
booleantrueoverscrollContain?boolean
Contain scroll input on overflowing axes.
booleanfalseScrollArea 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.
Related choices
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.