Appearance
ScrollFade
Visual indicators for scrollable content. Gradient fades appear at the top and bottom of scrollable containers to show users there's more content to explore.
Overview
ScrollFade provides gradient overlay components that automatically appear when content is scrollable. The fades smoothly animate in and out as users scroll, providing a polished, professional feel.
Key Features
- 🎨 Gradient Overlays: Smooth fade effects at top/bottom
- ✨ Auto-Detection: Automatically shows/hides based on scroll position
- 🎭 Blur Effect: Subtle backdrop blur for depth
- 🌈 Customizable: Custom gradients for different backgrounds
- 📱 Responsive: Works with any scroll container
- ⚡ Performant: Uses sticky positioning and CSS animations
Components
ScrollFade
Low-level component for individual fade overlays.
ScrollFadeContainer
All-in-one wrapper that handles scroll detection and fade rendering.
When to Use
Use ScrollFade when:
✅ Long lists - Indicate more items above/below
✅ Scrollable cards - Show content extends beyond visible area
✅ Modals/Dialogs - Long content that scrolls
✅ Dropdowns/Menus - Many options in a scrollable list
✅ Tables - Indicate more rows to scroll
Skip ScrollFade when:
❌ Short content - Everything fits without scrolling
❌ Obvious scrollbars - Native scrollbars are sufficient
❌ Performance critical - Every millisecond counts
Quick Start
Basic Example (ScrollFadeContainer)
The easiest way to add scroll fades:
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import SimpleBar from 'simplebar-react'
export function ScrollableList({ items }) {
return (
<ScrollFadeContainer>
<SimpleBar className="h-[400px]">
<div className="space-y-2 p-4">
{items.map((item) => (
<div key={item.id} className="p-4 bg-card rounded-lg">
{item.name}
</div>
))}
</div>
</SimpleBar>
</ScrollFadeContainer>
)
}Features in Detail
1. Automatic Scroll Detection
ScrollFadeContainer automatically detects scroll position:
tsx
<ScrollFadeContainer>
<div className="h-[300px] overflow-y-auto">
{/* Long content */}
</div>
</ScrollFadeContainer>Behavior:
- Top fade shows when scrolled down
- Bottom fade shows when more content below
- Fades smoothly animate in/out
2. Custom Fade Size
Control the height of the fade gradient:
tsx
<ScrollFadeContainer fadeSize="sm">
{/* Small fade (h-8) */}
</ScrollFadeContainer>
<ScrollFadeContainer fadeSize="md">
{/* Medium fade (h-12) - default */}
</ScrollFadeContainer>
<ScrollFadeContainer fadeSize="lg">
{/* Large fade (h-16) */}
</ScrollFadeContainer>3. Top or Bottom Only
Show fades on specific sides:
tsx
// Only top fade
<ScrollFadeContainer fades="top">
<SimpleBar className="h-[400px]">
{content}
</SimpleBar>
</ScrollFadeContainer>
// Only bottom fade
<ScrollFadeContainer fades="bottom">
<SimpleBar className="h-[400px]">
{content}
</SimpleBar>
</ScrollFadeContainer>
// Both (default)
<ScrollFadeContainer fades="both">
<SimpleBar className="h-[400px]">
{content}
</SimpleBar>
</ScrollFadeContainer>4. Custom Gradients
Match fades to your background color:
tsx
// For card backgrounds
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--card))',
via: 'hsl(var(--card) / 0.8)',
}}
>
<div className="bg-card rounded-lg">
{content}
</div>
</ScrollFadeContainer>
// For custom colors
<ScrollFadeContainer
fadeGradient={{
from: 'rgb(255 255 255)',
via: 'rgb(255 255 255 / 0.8)',
}}
>
{content}
</ScrollFadeContainer>5. Manual Control (ScrollFade)
For advanced use cases, use ScrollFade directly:
tsx
import { ScrollFade } from '@/shared/ui/components/scroll/ScrollFade'
import { useScrollFade } from '@/shared/ui/components/scroll/useScrollFade'
function CustomScrollable() {
const containerRef = useRef(null)
const { showTop, showBottom } = useScrollFade(containerRef)
return (
<div ref={containerRef} className="relative">
<ScrollFade position="top" visible={showTop} />
<div className="h-[400px] overflow-y-auto">
{content}
</div>
<ScrollFade position="bottom" visible={showBottom} />
</div>
)
}Common Patterns
Scrollable Card
tsx
import { ScrollFadeContainer } from '@/shared/ui/components/scroll/ScrollFadeContainer'
import { Card, CardHeader, CardTitle, CardContent } from '@/shadcn/components/ui/card'
function ActivityFeed({ activities }) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<ScrollFadeContainer
fadeGradient={{
from: 'hsl(var(--card))',
}}
>
<div className="h-[400px] overflow-y-auto space-y-3">
{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>
</ScrollFadeContainer>
</CardContent>
</Card>
)
}Dropdown Menu
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>
)
}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">
{/* Long terms content */}
</div>
</ScrollFadeContainer>
</DialogContent>
</Dialog>
)
}Data Table
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">
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.id}>
<td>{row.name}</td>
<td>{row.email}</td>
<td>{row.role}</td>
</tr>
))}
</tbody>
</table>
</div>
</ScrollFadeContainer>
</div>
)
}Best Practices
✅ Do's
- Match gradients - Use
fadeGradientto match container background - Use appropriate size -
smfor compact UIs,lgfor spacious layouts - Combine with SimpleBar - For custom scrollbars
- Test scroll behavior - Ensure fades appear/disappear correctly
- Consider dark mode - Use CSS variables for theme support
❌ Don'ts
- Don't overuse - Not every scrollable area needs fades
- Don't use on short content - Fades should indicate hidden content
- Don't forget z-index - Ensure fades layer correctly
- Don't use conflicting gradients - Match your design system
- Don't skip accessibility - Fades are decorative, ensure content is accessible
Styling
Default Gradient
css
/* Top fade */
background: linear-gradient(
to bottom,
hsl(var(--background)),
hsl(var(--background) / 0.8),
transparent
);
/* Bottom fade */
background: linear-gradient(
to top,
hsl(var(--background)),
hsl(var(--background) / 0.8),
transparent
);Custom Gradient
tsx
<ScrollFadeContainer
fadeGradient={{
from: 'rgb(255 255 255)', // Solid color at edge
via: 'rgb(255 255 255 / 0.8)', // 80% opacity in middle
// to: transparent (always)
}}
>
{content}
</ScrollFadeContainer>Blur Effect
The component includes a subtle backdrop-filter: blur(2px) for depth. The blur is masked to fade out with the gradient.
Performance
Optimizations
- Sticky positioning: No JavaScript for positioning
- CSS animations: Hardware-accelerated
- Debounced scroll: Prevents excessive updates
- Conditional rendering: Only renders when needed
Tips
- Use
fadeSize="sm"for better performance - Limit scroll detection frequency with
scrollOptions - Consider disabling on low-end devices
Accessibility
ScrollFade overlays are purely decorative:
aria-hidden="true": Hidden from screen readers- Pointer events disabled: Doesn't interfere with interaction
- Visual only: Content remains fully accessible
API Reference
For detailed prop documentation, see:
Related Components
- SimpleBar - Custom scrollbars
- ScrollArea (shadcn/ui) - Radix scroll area