Appearance
Scroll Patterns
Learn how to implement polished scrollable content with fade indicators, custom scrollbars, and smooth user experiences.
Overview
Katalyst provides scroll components for:
- ScrollFade: Gradient overlays indicating more content
- ScrollFadeContainer: All-in-one wrapper with automatic detection
- SimpleBar integration: Custom scrollbars
- Virtual scrolling: For large datasets
Basic Patterns
Scrollable Card with Fades
The most common pattern - a card with scrollable content and fade indicators:
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import { Card, CardHeader, CardTitle, CardContent } from '@/shadcn/components/ui/card'
import SimpleBar from 'simplebar-react'
function ActivityFeed({ activities }) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--card))',
}}
>
<SimpleBar className="h-[400px]">
<div className="space-y-3 pr-4">
{activities.map((activity) => (
<div key={activity.id} className="p-3 bg-muted rounded-md">
<p className="text-sm">{activity.message}</p>
<p className="text-xs text-muted-foreground">
{activity.timestamp}
</p>
</div>
))}
</div>
</SimpleBar>
</ScrollFadeContainer>
</CardContent>
</Card>
)
}Scrollable Sidebar
Common in dashboards - a fixed-height sidebar with scrollable content:
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import SimpleBar from 'simplebar-react'
function ShipmentSidebar({ shipments }) {
return (
<aside className="h-screen border-r">
<div className="p-4 border-b">
<h2 className="font-semibold">Shipments</h2>
</div>
<ScrollFadeContainer className="h-full">
<SimpleBar className="h-[calc(100vh-80px)]">
<div className="p-4 space-y-2">
{shipments.map((shipment) => (
<button
key={shipment.id}
className="w-full text-left p-3 rounded-lg hover:bg-accent"
>
<p className="font-medium">{shipment.id}</p>
<p className="text-sm text-muted-foreground">
{shipment.status}
</p>
</button>
))}
</div>
</SimpleBar>
</ScrollFadeContainer>
</aside>
)
}Dropdown and Menu Patterns
Scrollable Dropdown
For dropdowns with many options:
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
} from '@/shadcn/components/ui/dropdown-menu'
function LongDropdown({ items }) {
return (
<DropdownMenu>
<DropdownMenuTrigger>Select Item</DropdownMenuTrigger>
<DropdownMenuContent>
<ScrollFadeContainer
fadeSize="sm"
fadeGradient={{
from: 'hsl(var(--popover))',
}}
>
<div className="max-h-[300px] overflow-y-auto">
{items.map((item) => (
<DropdownMenuItem key={item.id}>
{item.name}
</DropdownMenuItem>
))}
</div>
</ScrollFadeContainer>
</DropdownMenuContent>
</DropdownMenu>
)
}Command Palette with Scroll
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import {
Command,
CommandInput,
CommandList,
CommandGroup,
CommandItem,
} from '@/shadcn/components/ui/command'
function CommandPalette() {
return (
<Command>
<CommandInput placeholder="Search..." />
<ScrollFadeContainer fadeSize="sm">
<CommandList className="max-h-[400px]">
<CommandGroup heading="Suggestions">
{/* Many items */}
</CommandGroup>
</CommandList>
</ScrollFadeContainer>
</Command>
)
}Table Patterns
Scrollable Table with Sticky Header
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
function ScrollableTable({ data }) {
return (
<div className="border rounded-lg">
<ScrollFadeContainer fades="bottom">
<div className="max-h-[500px] overflow-y-auto">
<table className="w-full">
<thead className="sticky top-0 bg-background z-10 border-b">
<tr>
<th className="p-3 text-left">Name</th>
<th className="p-3 text-left">Email</th>
<th className="p-3 text-left">Role</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id} className="border-b">
<td className="p-3">{row.name}</td>
<td className="p-3">{row.email}</td>
<td className="p-3">{row.role}</td>
</tr>
))}
</tbody>
</table>
</div>
</ScrollFadeContainer>
</div>
)
}Leaderboard Table
Used in dashboards for rankings:
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
function LeaderboardTable({ entries }) {
return (
<div className="rounded-lg border">
<div className="p-4 border-b">
<h3 className="font-semibold">Top Performers</h3>
</div>
<ScrollFadeContainer
fadeSize="md"
fadeGradient={{
from: 'hsl(var(--card))',
}}
>
<div className="max-h-[400px] overflow-y-auto">
<table className="w-full">
<thead className="sticky top-0 bg-card z-10">
<tr className="border-b">
<th className="p-3 text-left">Rank</th>
<th className="p-3 text-left">Name</th>
<th className="p-3 text-right">Score</th>
</tr>
</thead>
<tbody>
{entries.map((entry, index) => (
<tr key={entry.id} className="border-b hover:bg-muted/50">
<td className="p-3">#{index + 1}</td>
<td className="p-3">{entry.name}</td>
<td className="p-3 text-right font-medium">
{entry.score}
</td>
</tr>
))}
</tbody>
</table>
</div>
</ScrollFadeContainer>
</div>
)
}Modal and Dialog Patterns
Modal with Long Content
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/shadcn/components/ui/dialog'
function TermsDialog({ open, onOpenChange }) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Terms and Conditions</DialogTitle>
</DialogHeader>
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--background))',
}}
>
<div className="max-h-[60vh] overflow-y-auto prose prose-sm dark:prose-invert">
{/* Long terms content */}
</div>
</ScrollFadeContainer>
</DialogContent>
</Dialog>
)
}Advanced Patterns
Custom Scroll Detection
For advanced use cases, use the hook directly:
tsx
import { useRef } from 'react'
import { useScrollFade } from '@/shared/ui/components/scroll/useScrollFade'
import { ScrollFade } from '@/shared/ui/components/scroll/ScrollFade'
function CustomScrollable() {
const containerRef = useRef<HTMLDivElement>(null)
const { showTop, showBottom, isScrollable } = useScrollFade(containerRef, {
threshold: 20,
debounce: 150,
})
return (
<div ref={containerRef} className="relative">
{isScrollable && (
<>
<ScrollFade position="top" visible={showTop} />
<ScrollFade position="bottom" visible={showBottom} />
</>
)}
<div className="h-[400px] overflow-y-auto">
{/* Content */}
</div>
</div>
)
}Conditional Fades
Show fades only when needed:
tsx
function ConditionalFades({ items }) {
const containerRef = useRef<HTMLDivElement>(null)
const { showTop, showBottom, isScrollable } = useScrollFade(containerRef)
if (!isScrollable) {
return (
<div className="p-4">
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
)
}
return (
<div ref={containerRef} className="relative">
<ScrollFade position="top" visible={showTop} />
<div className="h-[400px] overflow-y-auto p-4">
{items.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</div>
<ScrollFade position="bottom" visible={showBottom} />
</div>
)
}Different Background Colors
Match fades to your design:
tsx
// For card backgrounds
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--card))',
}}
>
<div className="bg-card">{content}</div>
</ScrollFadeContainer>
// For popover backgrounds
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--popover))',
}}
>
<div className="bg-popover">{content}</div>
</ScrollFadeContainer>
// For custom colors
<ScrollFadeContainer
fadeGradient={{
from: 'rgb(255 255 255)',
via: 'rgb(255 255 255 / 0.8)',
}}
>
<div className="bg-white">{content}</div>
</ScrollFadeContainer>Best Practices
Gradient Matching
Always match the fade gradient to the container background:
tsx
// ✅ Good - matches card background
<Card>
<ScrollFadeContainer
fadeGradient={{ from: 'hsl(var(--card))' }}
>
{content}
</ScrollFadeContainer>
</Card>
// ❌ Bad - uses default background gradient on card
<Card>
<ScrollFadeContainer>
{content}
</ScrollFadeContainer>
</Card>Fade Size
Choose appropriate fade sizes:
tsx
// Small for compact UIs
<ScrollFadeContainer fadeSize="sm">
<DropdownMenu>{items}</DropdownMenu>
</ScrollFadeContainer>
// Medium for standard content (default)
<ScrollFadeContainer fadeSize="md">
<div className="h-[400px]">{content}</div>
</ScrollFadeContainer>
// Large for spacious layouts
<ScrollFadeContainer fadeSize="lg">
<div className="h-[600px]">{content}</div>
</ScrollFadeContainer>Performance
Optimize scroll performance:
tsx
// Increase debounce for less frequent updates
<ScrollFadeContainer
scrollOptions={{
debounce: 150, // Default is 50ms
}}
>
{content}
</ScrollFadeContainer>
// Adjust threshold to reduce toggle frequency
<ScrollFadeContainer
scrollOptions={{
threshold: 20, // Default is 10px
}}
>
{content}
</ScrollFadeContainer>Accessibility
Fades are decorative and don't affect accessibility:
- Automatically
aria-hidden="true" pointer-events: none- doesn't block interaction- Content remains fully accessible to screen readers
Related
- ScrollFade Component - Component guide
- ScrollFade API - API reference
- ScrollFadeContainer API - Container API
- useScrollFade Hook - Hook API
- DataTable Component - For virtual scrolling
- SimpleSortableTable - For table patterns