ScrollArea
Scrollable container with consistent custom scrollbar styling across all browsers and platforms. Includes keyboard navigation and configurable visibility options.
Cross-browserKeyboard NavigationCustom ScrollbarAccessibleDark Mode
Install
$
npx react-principles add scrollarea01
Live Demo
Explore all variants and interactive states in Storybook.
Open Storybookopen_in_newVertical Scrolling
Horizontal Scrolling
Scrollbar Visibility
Auto (default)
Always
Hover
Keyboard Navigation
Keyboard Shortcuts
- • Arrow Up/Down - Scroll by small amount
- • Page Up/Down - Scroll by page
- • Home/End - Jump to start/end
- • Tab - Focus on scrollable area
02
Code Snippet
src/ui/ScrollArea.tsx
import { ScrollArea } from "@/ui/ScrollArea"; // Vertical scroll (default) <ScrollArea className="h-64"> <p>Long content that scrolls vertically...</p> </ScrollArea> // Horizontal scroll <ScrollArea orientation="horizontal" className="w-96"> <div className="flex gap-4"> {/* Wide content that scrolls horizontally */} </div> </ScrollArea> // Both orientations <ScrollArea orientation="both" className="h-64 w-96"> <table> {/* Large table that scrolls both directions */} </table> </ScrollArea> // Always visible scrollbar <ScrollArea type="always" className="h-64"> {/* Scrollbar always visible */} </ScrollArea> // On hover <ScrollArea type="hover" className="h-64"> {/* Scrollbar appears on hover */} </ScrollArea>
03
Copy-Paste (Single File)
ScrollArea.tsx
import { cn } from "@/lib/utils"; // ─── Types ──────────────────────────────────────────────────────────────────── export type ScrollAreaOrientation = "vertical" | "horizontal" | "both"; export type ScrollAreaType = "auto" | "always" | "scroll" | "hover"; export interface ScrollAreaProps extends HTMLAttributes<HTMLDivElement> { orientation?: ScrollAreaOrientation; type?: ScrollAreaType; children: ReactNode; className?: string; } // ─── Constants ──────────────────────────────────────────────────────────────── const ORIENTATION_CLASSES: Record<ScrollAreaOrientation, string> = { vertical: "overflow-y-auto overflow-x-hidden", horizontal: "overflow-x-auto overflow-y-hidden", both: "overflow-auto", }; const TYPE_MODIFIERS: Record<ScrollAreaType, string> = { auto: "", always: "", scroll: "", hover: "overflow-hidden hover:overflow-auto", }; // ─── Component ──────────────────────────────────────────────────────────────── export function ScrollArea({ orientation = "vertical", type = "auto", children, className, onKeyDown, ...props }: ScrollAreaProps) { const scrollAreaRef = useRef<HTMLDivElement>(null); useEffect(() => { const element = scrollAreaRef.current; if (!element) return; const handleKeyDown = (e: KeyboardEvent) => { const scrollAmount = 64; const pageAmount = element.clientHeight * 0.9; switch (e.key) { case "ArrowDown": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollBy({ top: scrollAmount, behavior: "smooth" }); } break; case "ArrowUp": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollBy({ top: -scrollAmount, behavior: "smooth" }); } break; case "ArrowRight": if (orientation === "horizontal" || orientation === "both") { e.preventDefault(); element.scrollBy({ left: scrollAmount, behavior: "smooth" }); } break; case "ArrowLeft": if (orientation === "horizontal" || orientation === "both") { e.preventDefault(); element.scrollBy({ left: -scrollAmount, behavior: "smooth" }); } break; case "PageDown": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollBy({ top: pageAmount, behavior: "smooth" }); } break; case "PageUp": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollBy({ top: -pageAmount, behavior: "smooth" }); } break; case "Home": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollTo({ top: 0, behavior: "smooth" }); } break; case "End": if (orientation === "vertical" || orientation === "both") { e.preventDefault(); element.scrollTo({ top: element.scrollHeight, behavior: "smooth" }); } break; } if (onKeyDown) { const reactEvent = e as unknown as ReactKeyboardEvent<HTMLDivElement>; onKeyDown(reactEvent); } }; element.addEventListener("keydown", handleKeyDown); return () => element.removeEventListener("keydown", handleKeyDown); }, [orientation, onKeyDown]); return ( <div ref={scrollAreaRef} tabIndex={0} role="region" aria-orientation={orientation === "both" ? "vertical" : orientation} className={cn( "scrollarea-scrollbar", ORIENTATION_CLASSES[orientation], TYPE_MODIFIERS[type], className )} {...props} > {children} </div> ); }
04
Props
ScrollArea extends all standard div HTML attributes and accepts the following props:
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "vertical" | "horizontal" | "both" | vertical | Direction of scrolling. Vertical scrolls up/down, horizontal scrolls left/right, both allows scrolling in all directions. |
type | "auto" | "always" | "scroll" | "hover" | auto | Controls scrollbar visibility. Auto shows when needed, always keeps it visible, scroll forces it, hover shows on interaction. |
children | ReactNode | — | Content to display inside the scrollable area. |
className | string | — | Additional CSS classes to apply (merged with base styles). |
...props | HTMLAttributes<HTMLDivElement> | — | All standard div attributes like onClick, onFocus, etc. |
05
Accessibility
Keyboard Navigation
ScrollArea is fully keyboard accessible:
- Tab into the scrollable area to focus it
- Use Arrow keys for directional scrolling
- Page Up/Down for page-by-page navigation
- Home/End to jump to start or end
- Smooth scrolling for better UX
ARIA Attributes
Built-in accessibility features:
role="region"- Identifies as a scrollable regionaria-orientation- Indicates scroll directiontabIndex=0- Makes the area focusable- Orientation-aware keyboard handling
Screen Reader Support
Screen readers announce the scrollable region and its orientation. Users can navigate using standard keyboard shortcuts.