Skip to content

useScrollFade Hook API

React hook for detecting scroll position and determining when to show scroll fade indicators.

Import

tsx
import { useScrollFade, type UseScrollFadeOptions, type ScrollFadeState } from '@/shared/ui/components/scroll/useScrollFade'

Signature

ts
function useScrollFade(
  containerRef: RefObject<HTMLElement>,
  options?: UseScrollFadeOptions
): ScrollFadeState

Parameters

ParameterTypeDescription
containerRefRefObject<HTMLElement>Ref to the scrollable container
optionsUseScrollFadeOptionsOptional configuration

Returns

ScrollFadeState

ts
interface ScrollFadeState {
  /** Whether to show the top fade (content is scrolled down) */
  showTop: boolean
  /** Whether to show the bottom fade (more content below) */
  showBottom: boolean
  /** Whether the container is scrollable */
  isScrollable: boolean
}

Options

UseScrollFadeOptions

ts
interface UseScrollFadeOptions {
  /** Threshold in pixels to trigger fade visibility (default: 10) */
  threshold?: number
  /** Debounce delay in ms for scroll events (default: 50) */
  debounce?: number
  /** Enable debug logging */
  debug?: boolean
}

Features

  • Automatic detection: Monitors scroll position and container size
  • Debounced: Prevents excessive updates (default: 50ms)
  • Resize aware: Updates when container or content size changes
  • Threshold support: Configurable trigger distance from edges
  • Multi-library support: Works with SimpleBar, ScrollArea, and native overflow
  • Performance optimized: Uses passive event listeners and ResizeObserver

How It Works

  1. Finds the scrollable element:

    • SimpleBar: .simplebar-content-wrapper
    • Radix ScrollArea: [data-radix-scroll-area-viewport]
    • Native: Container with overflow
  2. Monitors scroll position:

    • Listens to scroll events (debounced)
    • Watches for resize changes
    • Calculates if content is scrollable
  3. Determines fade visibility:

    • showTop: scrollTop > threshold
    • showBottom: scrollTop + clientHeight < scrollHeight - threshold

Examples

Basic Usage

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 } = useScrollFade(containerRef)

  return (
    <div ref={containerRef} className="relative">
      <ScrollFade position="top" visible={showTop} />
      
      <div className="h-[400px] overflow-y-auto">
        {/* Scrollable content */}
      </div>
      
      <ScrollFade position="bottom" visible={showBottom} />
    </div>
  )
}

With Custom Options

tsx
const { showTop, showBottom, isScrollable } = useScrollFade(containerRef, {
  threshold: 20,   // Show fade when 20px from edge
  debounce: 150,   // Debounce scroll events by 150ms
  debug: true,     // Enable console logging
})

// Only render fades if content is scrollable
return (
  <div ref={containerRef} className="relative">
    {isScrollable && (
      <>
        <ScrollFade position="top" visible={showTop} />
        <ScrollFade position="bottom" visible={showBottom} />
      </>
    )}
    {/* Content */}
  </div>
)

With SimpleBar

tsx
import SimpleBar from 'simplebar-react'

function ScrollableList() {
  const containerRef = useRef<HTMLDivElement>(null)
  const { showTop, showBottom } = useScrollFade(containerRef)

  return (
    <div ref={containerRef} className="relative">
      <ScrollFade position="top" visible={showTop} />
      
      <SimpleBar className="h-[500px]">
        <div className="space-y-2 p-4">
          {items.map((item) => (
            <div key={item.id}>{item.name}</div>
          ))}
        </div>
      </SimpleBar>
      
      <ScrollFade position="bottom" visible={showBottom} />
    </div>
  )
}

With Radix ScrollArea

tsx
import { ScrollArea } from '@/shadcn/components/ui/scroll-area'

function ScrollableContent() {
  const containerRef = useRef<HTMLDivElement>(null)
  const { showTop, showBottom } = useScrollFade(containerRef)

  return (
    <div ref={containerRef} className="relative">
      <ScrollFade position="top" visible={showTop} />
      
      <ScrollArea className="h-[400px]">
        {content}
      </ScrollArea>
      
      <ScrollFade position="bottom" visible={showBottom} />
    </div>
  )
}

Conditional Rendering

tsx
const { showTop, showBottom, isScrollable } = useScrollFade(containerRef, {
  threshold: 10,
})

// Show a message if content isn't scrollable
if (!isScrollable) {
  return <div>All content is visible</div>
}

// Otherwise show with fades
return (
  <div ref={containerRef} className="relative">
    {showTop && <ScrollFade position="top" />}
    {/* Content */}
    {showBottom && <ScrollFade position="bottom" />}
  </div>
)

Implementation Details

Scroll Element Detection

The hook automatically detects the scrollable element:

ts
const getScrollElement = (): HTMLElement | null => {
  // Try SimpleBar first
  const simpleBarScroller = container.querySelector('.simplebar-content-wrapper')
  if (simpleBarScroller) return simpleBarScroller

  // Try ScrollArea (Radix)
  const radixViewport = container.querySelector('[data-radix-scroll-area-viewport]')
  if (radixViewport) return radixViewport

  // Fallback to container if it has overflow
  const hasOverflow = container.scrollHeight > container.clientHeight
  return hasOverflow ? container : null
}

Debouncing

Scroll events are debounced to improve performance:

ts
const debouncedCheck = () => {
  if (debounceTimerRef.current) {
    clearTimeout(debounceTimerRef.current)
  }
  debounceTimerRef.current = setTimeout(checkScroll, debounce)
}

Resize Observation

The hook watches for size changes using ResizeObserver:

ts
const resizeObserver = new ResizeObserver(() => {
  debouncedCheck()
})
resizeObserver.observe(scrollElement)

Performance

Optimizations

  • Passive event listeners: Improves scroll performance
  • Debounced updates: Reduces re-renders (default: 50ms)
  • ResizeObserver: Efficient size change detection
  • Cleanup: Properly removes listeners and observers

Tips

  • Increase debounce for less frequent updates
  • Use threshold to reduce unnecessary fade toggles
  • Consider isScrollable to avoid rendering fades when not needed

Type Definitions

ts
interface UseScrollFadeOptions {
  threshold?: number
  debounce?: number
  debug?: boolean
}

interface ScrollFadeState {
  showTop: boolean
  showBottom: boolean
  isScrollable: boolean
}

function useScrollFade(
  containerRef: RefObject<HTMLElement>,
  options?: UseScrollFadeOptions
): ScrollFadeState