Skip to content

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>
  )
}
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>
  )
}
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 fadeGradient to match container background
  • Use appropriate size - sm for compact UIs, lg for 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:



See Also