Scroll Area
Custom scrollbar component that provides a consistent, MD3 Expressive look and feel across all browsers.
The Scroll Area component augments native scroll functionality with a minimal, pill-shaped scrollbar that follows Material Design 3 Expressive guidelines. It ensures that scrollbars look and behave identically across different operating systems and browsers.
Introduction
Native scrollbars often conflict with modern UI designs, appearing bulky or inconsistent across platforms. The MD3 Expressive Scroll Area solves this by providing a highly customizable, themeable scrollbar that only appears when needed. It is built on top of Radix UI's Scroll Area primitives, ensuring high performance and excellent accessibility.
Anatomy
- Viewport: The visible area containing the scrollable content.
- Scrollbar: The track and thumb used to navigate the content.
- Thumb: The pill-shaped indicator that moves as the user scrolls.
- Corner: The intersection where both horizontal and vertical scrollbars meet.
Variants
Orientations
- Vertical (Default): For content that exceeds the height of its container.
- Horizontal: For wide content like data tables or image galleries.
- Both: Enables both vertical and horizontal scrolling with a dedicated corner element.
Features
Visibility Behaviors
Control when the scrollbar is displayed using the type prop:
- Scroll (Default): Appears while scrolling and hides after a delay. Recommended for all devices and touch interactions.
- Hover: Scrollbar appears only when hovering over the container (desktop-focused).
- Always: Scrollbar is permanently visible.
- None: Hides scrollbar UI completely while keeping native scrolling enabled.
Custom Sizing & Colors
You can flexibly customize the thickness, colors, and border radius of the scrollbars using dedicated styling props:
scrollbarSize: Set scrollbar track thickness in pixels (e.g.6for slim,14for high visibility).thumbClassName: Additional Tailwind CSS classes applied to the thumb element (overrides background colors, hover/active states, and border radius viatailwind-merge).trackClassName: Tailwind CSS classes applied to the scrollbar track container.cornerClassName: Tailwind CSS classes applied to the corner element when both orientations are active.verticalScrollbarProps&horizontalScrollbarProps: Override styling and size individually per scroll axis.
Expressive Styling
The scrollbar thumb is a pill-shaped element styled with Material Design 3 surface tokens (bg-m3-on-surface/25). It smoothly transitions to darker contrast on hover (hover:bg-m3-on-surface/40) and active drag (active:bg-m3-on-surface/55), while providing an accessible 44px touch target.
Usage
Basic Usage
import { ScrollArea } from "@bug-on/m3-expressive/layout";
<ScrollArea className="h-72 w-48 rounded-md border">
<div className="p-4">
<h4>Tags</h4>
{tags.map((tag) => (
<div key={tag} className="text-sm">
{tag}
</div>
))}
</div>
</ScrollArea>
Custom Sizing and Colors
import { ScrollArea } from "@bug-on/m3-expressive/layout";
<ScrollArea
scrollbarSize={8}
thumbClassName="bg-m3-primary/60 hover:bg-m3-primary rounded-full"
trackClassName="bg-m3-surface-container"
className="h-64 w-80 rounded-m3-md border border-m3-outline-variant"
>
<div className="p-4">
{/* Scrollable content */}
</div>
</ScrollArea>
Custom Scrollbar Composition
You can compose custom layouts directly using ScrollArea, ScrollAreaScrollbar, and ScrollAreaCorner:
import {
ScrollArea,
ScrollAreaScrollbar,
ScrollAreaCorner,
} from "@bug-on/m3-expressive/layout";
<ScrollArea orientation="both" className="h-64 w-full">
<div className="w-[1200px] h-[800px] p-4">Large canvas content</div>
<ScrollAreaScrollbar orientation="vertical" scrollbarSize={8} />
<ScrollAreaScrollbar orientation="horizontal" scrollbarSize={8} />
<ScrollAreaCorner />
</ScrollArea>
Best Practices
Do
- Use Scroll Area for containers with a fixed height where content may overflow.
- Prefer
type="scroll"for smooth, native-like mobile and desktop scrolling. - Use
type="always"for critical scrollable areas (like sidebars or code blocks) where users need immediate visual orientation. - Use
scrollbarSizeand semantic MD3 tokens (bg-m3-primary,bg-m3-surface-container) to match the surrounding layout theme. - Set a clear
max-heightorheighton the Scroll Area container.
Don't
- Don't use Scroll Area for the entire page body; let the browser handle top-level scrolling.
- Don't use
orientation="both"if only one axis is likely to overflow. - Avoid nesting multiple Scroll Areas within each other, as it can be confusing for users to navigate.
Accessibility
- Roles: Correctly implements ARIA roles for scroll containers and regions.
- Keyboard: Supports standard keyboard navigation (Arrow keys, Page Up/Down, Home/End).
- Touch: Provides native-feeling momentum scrolling on touch devices with accessible 44px min touch target on thumb.
- Visibility: The scrollbar is a visual aid and doesn't replace native accessibility features.
API Reference
ScrollArea
| Prop | Type | Default | Description |
|---|---|---|---|
type | "scroll" | "hover" | "always" | "none" | "scroll" | Visibility behavior of scrollbars. |
orientation | "vertical" | "horizontal" | "both" | "vertical" | Scroll axis to render. |
scrollbarSize | number | 10 | Thickness of scrollbar track in px (width for vertical, height for horizontal). |
thumbClassName | string | — | Tailwind CSS classes for the thumb element (merges via tailwind-merge). |
trackClassName | string | — | Tailwind CSS classes for the scrollbar track container. |
cornerClassName | string | — | CSS classes for the corner element when both scrollbars are visible. |
verticalScrollbarProps | Omit<ScrollAreaScrollbarProps, "orientation"> | — | Props forwarded specifically to the vertical scrollbar. |
horizontalScrollbarProps | Omit<ScrollAreaScrollbarProps, "orientation"> | — | Props forwarded specifically to the horizontal scrollbar. |
thumbProps | ComponentPropsWithoutRef<typeof RadixScrollArea.Thumb> | — | Extra props forwarded directly to inner thumb elements. |
scrollHideDelay | number | 600 | Delay in ms before scrollbars hide when type="scroll" or type="hover". |
viewportClassName | string | — | Extra classes applied to the inner scrolling viewport element. |
viewportRef | React.Ref<HTMLDivElement> | — | Ref to the scrolling viewport element. |
viewportProps | ComponentPropsWithoutRef<typeof RadixScrollArea.Viewport> | — | Extra props applied to the inner viewport. |
className | string | — | Custom classes applied to the root container. |
ScrollAreaScrollbar
Standalone scrollbar component for custom composition.
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "vertical" | Scrollbar orientation ("vertical" or "horizontal"). |
scrollbarSize | number | 10 | Thickness in pixels (width or height). |
thumbClassName | string | — | Tailwind CSS classes for the thumb element. |
trackClassName | string | — | Tailwind CSS classes for the track element. |
thumbRef | React.Ref<React.ElementRef<typeof RadixScrollArea.Thumb>> | — | Ref forwarded directly to the inner thumb element. |
thumbProps | ComponentPropsWithoutRef<typeof RadixScrollArea.Thumb> | — | Extra props forwarded to the thumb element. |
className | string | — | Classes applied to the scrollbar container. |
ScrollAreaCorner
Intersection corner component rendered when both scrollbars are active.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Classes applied to the corner element (default: bg-m3-surface-container). |