npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gboue/use-scroll-fades

v3.4.0

Published

Library-agnostic React hook that adds scroll-fade indicators to any scrollable container (vertical and horizontal)

Readme

use-scroll-fades

npm version npm downloads

Library-agnostic React hook that adds scroll-fade indicators to any scrollable container. Supports both vertical and horizontal scrolling with smooth animations. No CSS frameworks, no runtime dependencies. Works with plain CSS-in-JS, inline styles, or your existing styling solution.

  • Vertical scrolling: Top/bottom fades for vertical content
  • Horizontal scrolling: Left/right fades for horizontal content
  • 🎨 Custom gradient colors: Full control over fade colors and opacity
  • ♿ Accessibility support: Auto-respects prefers-reduced-motion and browser capabilities
  • Smart hiding: Fades automatically hide at scroll edges
  • Smooth animations: Built-in CSS transitions with cross-browser support
  • Interactive demos: Live color picker and examples at demo site
  • Zero external deps, React only
  • Ships with TypeScript definitions

Installation

# npm
npm i @gboue/use-scroll-fades
# or
yarn add @gboue/use-scroll-fades
# or
pnpm add @gboue/use-scroll-fades

Types are included. If you use JavaScript only, no extra step is required. If you prefer separate type installs in some setups, the package already contains *.d.ts files.


Visual Examples

See the library in action with different scrolling scenarios at the official github page: https://cosmicthreepointo.github.io/use-scroll-fades/

Vertical Scrolling with Gradient Background ✨

Vertical Scrolling with Gradient Background

⚠️ Top and bottom fades that works perfectly with complex gradient backgrounds!

Horizontal Scrolling with Image Pattern 🎨

Horizontal Scrolling with Image Pattern

🎨 Left and right fades work seamlessly with background images and patterns!

Combined Vertical + Horizontal Scrolling 🚀

Combined Vertical + Horizontal Scrolling

🚀 All four fades (top, bottom, left, right) work together automatically. No configuration needed!

Custom Color Gradients 🌈

Custom Color Gradients

🌈 Customizable gradient colors with full control over fade appearance and styling!


Quick Start (New Mask-Image Approach) ✨

The new mask-image approach provides true transparency that works with any background:

import { useScrollFades } from '@gboue/use-scroll-fades'

function ScrollableList({ items }) {
  const { containerRef, getContainerStyle } = useScrollFades({
    threshold: 16,
    fadeSize: 20,
    transitionDuration: 300
  })

  return (
    <div 
      ref={containerRef}
      style={{
        height: '400px',
        width: 'fit-content', // Fits content width - no horizontal scroll
        minWidth: '300px',    // Minimum width for readability
        overflow: 'auto',
        // Complex background that works perfectly with mask-image
        backgroundImage: 'linear-gradient(45deg, #ff6b6b, #4ecdc4)',
        ...getContainerStyle()
      }}
    >
      {items.map(item => (
        <div key={item.id} className="item" style={{ padding: '1rem' }}>
          {item.name}
        </div>
      ))}
    </div>
  )
}

🎨 Gradient Color Customization (New!)

Customize the fade colors to match your design theme with the new gradient color API:

Simple Color Override

import { useScrollFades } from '@gboue/use-scroll-fades'

function ColoredScrollableList({ items }) {
  const { 
    containerRef, 
    getContainerStyle,
    getGradientProperties,
    getColoredFadeClass
  } = useScrollFades({
    fadeColor: 'rgba(0, 122, 255, 0.3)', // Blue fade matching your theme
    threshold: 16,
    fadeSize: 24
  })

  return (
    <div 
      ref={containerRef}
      className={getColoredFadeClass()} // Add colored fade support
      style={{
        height: '400px',
        overflow: 'auto',
        background: 'linear-gradient(135deg, #667eea, #764ba2)',
        ...getContainerStyle(),
        ...getGradientProperties() // Apply colored gradients
      }}
    >
      {items.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  )
}

Individual Direction Colors

import { useScrollFades } from '@gboue/use-scroll-fades'

function CustomGradientList({ items }) {
  const { 
    containerRef, 
    getContainerStyle,
    getGradientProperties,
    getColoredFadeClass
  } = useScrollFades({
    topColors: { from: 'rgba(255, 0, 0, 0.4)', to: 'transparent' },
    bottomColors: { from: 'rgba(0, 255, 0, 0.4)', to: 'transparent' },
    leftColors: { from: 'rgba(0, 0, 255, 0.4)', to: 'transparent' },
    rightColors: { from: 'rgba(255, 255, 0, 0.4)', to: 'transparent' }
  })

  return (
    <div 
      ref={containerRef}
      className={getColoredFadeClass()}
      style={{
        height: '400px',
        overflow: 'auto',
        ...getContainerStyle(),
        ...getGradientProperties()
      }}
    >
      {items.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  )
}

Advanced Custom Color Example (Overlay Method)

For complex scenarios where you need full control over positioning and effects:

import React, { useRef, useEffect } from 'react';
import { useScrollFades } from '@gboue/use-scroll-fades';

function AdvancedColorFadeList({ items }) {
  const { containerRef, state } = useScrollFades({
    threshold: 8,
    fadeSize: 50,
    transitionDuration: 300
  });

  const topFadeRef = useRef(null);
  const bottomFadeRef = useRef(null);

  // Custom colors with opacity
  const topColor = 'rgba(255, 69, 0, 0.2)';    // Orange fade
  const bottomColor = 'rgba(0, 128, 255, 0.2)'; // Blue fade

  // Position overlays to match container
  useEffect(() => {
    const updatePositions = () => {
      if (!containerRef.current) return;
      
      const rect = containerRef.current.getBoundingClientRect();
      const style = window.getComputedStyle(containerRef.current);
      
      if (topFadeRef.current) {
        topFadeRef.current.style.top = `${rect.top}px`;
        topFadeRef.current.style.left = `${rect.left}px`;
        topFadeRef.current.style.width = `${rect.width}px`;
        topFadeRef.current.style.borderRadius = style.borderRadius;
      }
      
      if (bottomFadeRef.current) {
        bottomFadeRef.current.style.bottom = `${window.innerHeight - rect.bottom}px`;
        bottomFadeRef.current.style.left = `${rect.left}px`;
        bottomFadeRef.current.style.width = `${rect.width}px`;
        bottomFadeRef.current.style.borderRadius = style.borderRadius;
      }
    };

    updatePositions();
    const interval = setInterval(updatePositions, 100);
    
    return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ position: 'relative' }}>
      <div 
        ref={containerRef}
        style={{
          height: '400px',
          overflow: 'auto',
          background: '#ffffff',
          borderRadius: '16px',
          border: '1px solid #e2e8f0'
        }}
      >
        {items.map(item => (
          <div key={item.id} style={{ padding: '1rem', margin: '1rem' }}>
            {item.content}
          </div>
        ))}
      </div>

      {/* Colored fade overlays */}
      <div 
        ref={topFadeRef}
        style={{
          position: 'fixed',
          height: '50px',
          background: `linear-gradient(to bottom, ${topColor}, transparent)`,
          display: state.showTop ? 'block' : 'none',
          pointerEvents: 'none',
          zIndex: 10
        }}
      />
      <div 
        ref={bottomFadeRef}
        style={{
          position: 'fixed',
          height: '50px',
          background: `linear-gradient(to top, ${bottomColor}, transparent)`,
          display: state.showBottom ? 'block' : 'none',
          pointerEvents: 'none',
          zIndex: 10
        }}
      />
    </div>
  );
}

Interactive Demo

Try the live interactive color picker demo at: https://cosmicthreepointo.github.io/use-scroll-fades/

Legacy Overlay Approach (Deprecated)

For backward compatibility, the overlay approach is still available but deprecated:

import { useScrollFades } from '@gboue/use-scroll-fades'

function LegacyScrollableList({ items }) {
  const { containerRef, state, getOverlayStyle } = useScrollFades({
    threshold: 16,
    transitionDuration: 300
  })

  return (
    <div style={{ position: 'relative' }}>
      <div 
        ref={containerRef}
        style={{ height: '400px', overflow: 'auto' }}
      >
        {items.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
      
      {/* Overlay elements - only work well on solid backgrounds */}
      <div 
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          right: 0,
          height: '20px',
          ...getOverlayStyle('top')
        }}
      />
      <div 
        style={{
          position: 'absolute',
          bottom: 0,
          left: 0,
          right: 0,
          height: '20px',
          ...getOverlayStyle('bottom')
        }}
      />
    </div>
  )
}

Why Mask-Image is Superior ✨

✅ New Mask-Image Approach (Recommended)

  • 🎨 True transparency: Works perfectly with any background (images, gradients, patterns)
  • 🚀 Better performance: GPU-accelerated mask properties, fewer DOM elements
  • 📐 Simpler markup: Apply styles directly to the scrollable container
  • 🔧 Cleaner CSS: Uses modern CSS mask properties
  • 🌐 Cross-browser: Includes WebKit prefixes for Safari support
  • ⚡ Future-proof: Modern CSS approach that will improve over time

❌ Old Overlay Approach (Deprecated)

  • 🎯 Limited backgrounds: Only works well on solid colors
  • 💔 Visual artifacts: Creates muddy, layered effects on complex backgrounds
  • 📦 More DOM elements: Requires separate positioned overlay divs
  • 🔧 Positioning complexity: Absolute positioning can be tricky to maintain
  • 🐛 Z-index issues: Can interfere with other content

Real-World Examples

Complex Gradient Background ✨

// This would look terrible with overlay gradients!
<div 
  ref={containerRef}
  style={{
    height: '400px',
    overflow: 'auto',
    backgroundImage: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
    ...getContainerStyle() // ✅ Perfect transparency with mask-image!
  }}
>
  {/* Your scrollable content */}
</div>

Image/Pattern Backgrounds ✨

// Pattern backgrounds work beautifully!
<div 
  ref={containerRef}
  style={{
    overflow: 'auto',
    backgroundImage: 'url("your-pattern.svg")',
    backgroundColor: '#f8fafc',
    ...getContainerStyle() // ✅ True transparency shows the pattern perfectly!
  }}
>
  {/* Your content */}
</div>

Combined Vertical + Horizontal Scrolling ✨

The mask-image approach automatically handles both vertical and horizontal scrolling simultaneously - no configuration needed!

Browser Support

The package works in all modern browsers with full support for both mask-image and overlay approaches:

✅ Mask-Image Approach (Recommended)

  • Chrome 120+ - Full mask-image support
  • Firefox 53+ - Full mask-image support
  • Safari 15.4+ - Full mask-image support with WebKit prefixes
  • Edge 79+ - Full mask-image support

✅ Legacy Overlay Approach (Fallback)

  • Chrome 26+ (2013+) - Full animation support
  • Firefox 16+ (2012+) - Full animation support
  • Safari 9+ (2015+) - Full animation support
  • Edge 12+ (2015+) - Full animation support
  • iOS Safari 9+ (2015+) - Full animation support
  • Android Chrome 26+ (2013+) - Full animation support

🔧 Graceful Degradation

  • Internet Explorer 11 - Overlay approach works, no smooth transitions
  • Older browsers - Falls back to instant show/hide, maintains scrolling functionality

For browsers without mask-image support, the hook automatically falls back to the overlay approach or gracefully degrades to no fade effects while maintaining full scrolling functionality.

Technical Details

The animations use CSS opacity transitions with vendor prefixes for maximum compatibility:

transition: opacity 200ms ease-out;
-webkit-transition: opacity 200ms ease-out; /* Safari */
-moz-transition: opacity 200ms ease-out;    /* Firefox */
-ms-transition: opacity 200ms ease-out;     /* IE/Edge */

Performance optimized:

  • Uses opacity (GPU-accelerated) instead of display changes
  • pointer-events: none prevents interaction overhead
  • requestAnimationFrame for smooth scroll handling
  • ResizeObserver and MutationObserver for efficient updates

Accessibility features:

  • aria-hidden="true" on overlay elements
  • Respects prefers-reduced-motion when you disable transitions
  • No interference with keyboard navigation or screen readers

Horizontal Scrolling

Both approaches support horizontal scrolling:

const { containerRef, getContainerStyle } = useScrollFades({
  threshold: 12,
  fadeSize: 24
})

return (
  <div 
    ref={containerRef}
    style={{
      width: '400px',
      height: '200px',
      overflow: 'auto',
      ...getContainerStyle()
    }}
  >
    <div style={{ width: '800px', height: '100px' }}>
      Wide content that scrolls horizontally
    </div>
  </div>
)

Configuration Options

export type FadeState = { 
  showTop: boolean,    // Vertical scroll: top fade visibility
  showBottom: boolean, // Vertical scroll: bottom fade visibility  
  showLeft: boolean,   // Horizontal scroll: left fade visibility
  showRight: boolean   // Horizontal scroll: right fade visibility
}

export type GradientColors = {
  from: string // Start color (solid edge), default 'rgba(0,0,0,0.15)'
  to: string   // End color (fade to transparent), default 'transparent'
}

export type UseScrollFadesOptions = {
  // Basic configuration
  threshold?: number // px edge tolerance, default 8
  fadeSize?: number // Size of fade effect in pixels, default 20
  // 🎨 NEW: Gradient color customization
  fadeColor?: string // Single color for all directions, overrides individual colors
  topColors?: GradientColors // Custom gradient colors for top fade
  bottomColors?: GradientColors // Custom gradient colors for bottom fade
  leftColors?: GradientColors // Custom gradient colors for left fade
  rightColors?: GradientColors // Custom gradient colors for right fade
  // Legacy overlay gradients (deprecated)
  topGradient?: string // CSS gradient for the top overlay
  bottomGradient?: string // CSS gradient for the bottom overlay
  leftGradient?: string // CSS gradient for the left overlay
  rightGradient?: string // CSS gradient for the right overlay
  // Animation options
  transitionDuration?: number // Fade transition duration in ms, default 200
  transitionTimingFunction?: string // CSS timing function, default 'ease-out'
  disableTransitions?: boolean // Disable animations, default false
  // Accessibility and browser support
  respectReducedMotion?: boolean // Auto-respect prefers-reduced-motion, default true
  respectBrowserSupport?: boolean // Disable if mask-image unsupported, default true
  maskImageFallback?: 'disable' | 'ignore' // Fallback behavior, default 'disable'
}

export function useScrollFades<T extends HTMLElement = HTMLElement>(
  options?: UseScrollFadesOptions
): {
  containerRef: React.RefObject<T>
  state: FadeState
  /**
   * ✨ PRIMARY: Get styles for the scrollable container with mask-based fades
   * Creates true transparency that works with any background!
   */
  getContainerStyle: (state?: FadeState) => React.CSSProperties
  /**
   * 🚫 DEPRECATED: Legacy overlay approach for backward compatibility
   * Use getContainerStyle() instead - it's better in every way!
   */
  getOverlayStyle: (
    position: 'top' | 'bottom' | 'left' | 'right',
    state?: FadeState
  ) => React.CSSProperties
  /**
   * 🎨 NEW: Get CSS custom properties for colored gradient fades
   * Use with getColoredFadeClass() for colored fade effects
   */
  getGradientProperties: (state?: FadeState) => Record<string, string>
  /**
   * 🎨 NEW: Get CSS class name for colored fades
   * Add this class to your container along with getContainerStyle()
   */
  getColoredFadeClass: () => string
  /**
   * 🔧 Accessibility and browser compatibility information
   */
  accessibility: {
    shouldApplyEffects: boolean // Whether effects are enabled based on user prefs
    reducedMotionPreferred: boolean // User's motion preference state
    browserCapabilities: object // Detailed browser capability detection
    shouldDisableTransitions: boolean // Whether transitions should be disabled
  }
}

Notes

  • Agnostic: The hook returns state and a small helper. You are free to style overlays with CSS classes, CSS modules, styled components, etc. The gradients are plain CSS strings you can override.
  • Accessibility: Automatically respects prefers-reduced-motion and checks browser support for graceful degradation.
  • Performance: Uses requestAnimationFrame, ResizeObserver, and MutationObserver to stay accurate during resizes and dynamic content changes without jank.
  • Smooth Animations: Built-in CSS transitions provide smooth fade in/out effects with cross-browser support.

♿ Accessibility & Browser Support

The library automatically respects user accessibility preferences and browser capabilities:

Accessibility Features

  • 🎯 prefers-reduced-motion Support: Automatically disables transitions when users prefer reduced motion
  • 🌐 Browser Detection: Checks for CSS mask-image support before applying effects
  • ⚡ Progressive Enhancement: Falls back gracefully in unsupported browsers
  • 🔧 Manual Control: Override automatic behavior when needed
function AccessibleScrollList() {
  const { containerRef, getContainerStyle, accessibility } = useScrollFades({
    respectReducedMotion: true,      // Default: true - respects user preference
    respectBrowserSupport: true,     // Default: true - checks browser capabilities  
    maskImageFallback: 'disable'    // Default: 'disable' - what to do if unsupported
  })
  
  // Access detailed accessibility information
  console.log(accessibility.shouldApplyEffects)      // false if user prefers reduced motion
  console.log(accessibility.reducedMotionPreferred)  // current user preference
  console.log(accessibility.browserCapabilities)     // detailed browser info
  console.log(accessibility.shouldDisableTransitions) // whether transitions are disabled

  return (
    <div 
      ref={containerRef}
      style={{
        height: '300px',
        overflow: 'auto',
        ...getContainerStyle()  // Will be empty if effects should not apply
      }}
    >
      {/* Your content */}
    </div>
  )
}

Browser Compatibility

✅ Full Support:

  • Chrome 120+ (mask-image stable)
  • Firefox 53+ (mask-image stable)
  • Safari 15.4+ (mask-image stable)
  • Edge 79+ (Chromium-based)

⚠️ Partial Support:

  • Safari 3.1+ (with -webkit- prefix)
  • Legacy browsers: Falls back gracefully (no effects, no errors)

🔧 Manual Feature Detection:

import { supportsMaskImage, prefersReducedMotion } from '@gboue/use-scroll-fades'

// Check browser capabilities manually
if (supportsMaskImage()) {
  console.log('Mask-image is supported!')
}

if (prefersReducedMotion()) {
  console.log('User prefers reduced motion')
}

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | respectReducedMotion | boolean | true | Automatically respect prefers-reduced-motion: reduce | | respectBrowserSupport | boolean | true | Disable effects if mask-image is not supported | | maskImageFallback | 'disable' \| 'ignore' | 'disable' | What to do when mask-image is unsupported |


Animation Options

The hook includes smooth fade animations by default. You can customize or disable them:

import { useScrollFades } from '@gboue/use-scroll-fades'

// Default smooth animations (200ms ease-out)
const { containerRef, state, getOverlayStyle } = useScrollFades()

// Custom animation timing
const customAnimated = useScrollFades({
  transitionDuration: 300, // 300ms duration
  transitionTimingFunction: 'ease-in-out' // Custom easing
})

// Faster animations
const quickFades = useScrollFades({
  transitionDuration: 100, // Snappy 100ms
  transitionTimingFunction: 'ease-out'
})

// Disable animations entirely
const noAnimations = useScrollFades({
  disableTransitions: true // Instant show/hide
})

// Custom CSS transitions (for advanced users)
const advancedAnimations = useScrollFades({
  transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)' // Material Design easing
})

Supported CSS Timing Functions

All standard CSS timing functions are supported across modern browsers:

  • ease, ease-in, ease-out, ease-in-out
  • linear
  • cubic-bezier(n,n,n,n) for custom curves
  • steps(n, start|end) for stepped animations

Advanced Usage

Custom Styling with CSS Classes

import React from 'react'
import { useScrollFades } from '@gboue/use-scroll-fades'
import './ScrollFades.css' // Your custom CSS

export default function CustomStyledList({ items }) {
  const { containerRef, state } = useScrollFades({ threshold: 16 })

  return (
    <div className="scroll-container">
      <div ref={containerRef} className="scrollable-content">
        {items.map((item, i) => (
          <div key={i} className="list-item">{item}</div>
        ))}
      </div>
      
      {/* Custom CSS classes instead of inline styles */}
      <div className={`fade-overlay fade-top ${state.showTop ? 'visible' : 'hidden'}`} />
      <div className={`fade-overlay fade-bottom ${state.showBottom ? 'visible' : 'hidden'}`} />
    </div>
  )
}

With Styled Components

import React from 'react'
import styled from 'styled-components'
import { useScrollFades } from '@gboue/use-scroll-fades'

const Container = styled.div`
  position: relative;
  max-width: 640px;
`

const ScrollableArea = styled.div`
  overflow: auto;
  height: 300px;
  background: white;
  border-radius: 8px;
`

const FadeOverlay = styled.div<{ $visible: boolean; $position: 'top' | 'bottom' }>`
  position: absolute;
  left: 0;
  right: 0;
  height: 40px;
  pointer-events: none;
  opacity: ${props => props.$visible ? 1 : 0};
  transition: opacity 0.2s ease;
  
  ${props => props.$position === 'top' ? `
    top: 0;
    background: linear-gradient(to bottom, rgba(255,255,255,0.9), transparent);
  ` : `
    bottom: 0;
    background: linear-gradient(to top, rgba(255,255,255,0.9), transparent);
  `}
`

export default function StyledList({ items }) {
  // Note: Hook provides built-in smooth transitions
  // But you can also use styled-components transitions as shown above
  const { containerRef, state, getOverlayStyle } = useScrollFades({ 
    threshold: 10,
    transitionDuration: 250 // Slightly slower for demo
  })

  return (
    <Container>
      <ScrollableArea ref={containerRef}>
        {items.map((item, i) => <div key={i}>{item}</div>)}
      </ScrollableArea>
      
      {/* Option 1: Use hook's built-in animations */}
      <div style={{
        position: 'absolute', left: 0, right: 0, top: 0, height: 40,
        pointerEvents: 'none',
        ...getOverlayStyle('top')
      }} />
      <div style={{
        position: 'absolute', left: 0, right: 0, bottom: 0, height: 40,
        pointerEvents: 'none', 
        ...getOverlayStyle('bottom')
      }} />
      
      {/* Option 2: Or use styled-components animations */}
      {/* <FadeOverlay $visible={state.showTop} $position="top" />
      <FadeOverlay $visible={state.showBottom} $position="bottom" /> */}
    </Container>
  )
}

Virtualized Lists (React Window)

import React from 'react'
import { FixedSizeList as List } from 'react-window'
import { useScrollFades } from '@gboue/use-scroll-fades'

export default function VirtualizedList({ items }) {
  const { containerRef, state, getOverlayStyle } = useScrollFades()

  return (
    <div style={{ position: 'relative', height: 400, width: 300 }}>
      <List
        ref={containerRef}
        height={400}
        itemCount={items.length}
        itemSize={50}
      >
        {({ index, style }) => (
          <div style={style}>
            {items[index]}
          </div>
        )}
      </List>
      
      <div aria-hidden style={{
        ...getOverlayStyle('top', state),
        position: 'absolute', top: 0, left: 0, right: 0, height: 30
      }} />
      <div aria-hidden style={{
        ...getOverlayStyle('bottom', state),
        position: 'absolute', bottom: 0, left: 0, right: 0, height: 30
      }} />
    </div>
  )
}

Accessibility Tips

  • Keep overlays aria-hidden so screen readers ignore them
  • Avoid placing tabbable elements inside fade overlays
  • Consider adding aria-live status text like "More content below" if your UX requires it
  • Test with keyboard navigation to ensure overlays don't interfere

Performance Tips

  • The hook automatically throttles scroll events using requestAnimationFrame
  • ResizeObserver and MutationObserver ensure fades update when content changes
  • State updates are optimized to prevent unnecessary re-renders
  • No external dependencies means smaller bundle size

FAQ

Does it work without any CSS library?
Yes. The hook returns state plus getOverlayStyle. You can replace those inline styles with your own classes if you prefer.

Can I use it in virtualized lists (e.g., React Window)?
Yes. Attach containerRef to the scrollable viewport element and it will work the same.

Is SSR supported?
Yes. The hook only touches DOM APIs inside useEffect, so it's safe on the server; fades initialize on the client.

How do I theme the gradients?
Override topGradient and bottomGradient with any valid CSS gradient or image.

Does it support horizontal scrolling?
Yes! Supports both vertical and horizontal scrolling automatically with the mask-image approach. No configuration needed!

How do I migrate from overlay to mask-image approach?
Simple! Replace getOverlayStyle() overlays with getContainerStyle() applied directly to your container:

// OLD overlay approach ❌
<div style={{ position: 'relative' }}>
  <div ref={containerRef} style={{ overflow: 'auto' }}>
    {content}
  </div>
  <div style={{ position: 'absolute', top: 0, ...getOverlayStyle('top') }} />
  <div style={{ position: 'absolute', bottom: 0, ...getOverlayStyle('bottom') }} />
</div>

// NEW mask-image approach ✅  
<div ref={containerRef} style={{ overflow: 'auto', ...getContainerStyle() }}>
  {content}
</div>

What React versions are supported?
React 16.8+ (hooks support). Tested with React 17, 18, and 19.

Can I use it with CSS-in-JS libraries?
Absolutely. The hook is styling-agnostic - use it with styled-components, emotion, or any CSS solution.


Contributing

Issues and pull requests are welcome! Please ensure tests pass before submitting.

# Development setup
npm install
npm run test
npm run build

License

MIT