Skip to main content

useCarouselScroll

The useCarouselScroll hook manages the scrolling behavior of carousel components, providing smooth navigation with support for touch gestures, keyboard controls, and automatic scrolling. It’s specifically designed for anime banner carousels but can be adapted for any carousel use case.

Type Signature

function useCarouselScroll(
  banners: AnimeBannerInfo[] | null,
  currentIndex: number,
  setCurrentIndex: (index: number) => void
): {
  bannerContainerRef: RefObject<HTMLDivElement>
  handleTouchStart: (e: React.TouchEvent) => void
  handleTouchMove: (e: React.TouchEvent) => void
  handleTouchEnd: () => void
  handlePrev: () => void
  handleNext: () => void
  resetInterval: () => void
  intervalRef: MutableRefObject<number | null>
  handleScroll: (index: number) => void
  handleKeyDown: (e: KeyboardEvent) => void
}

Parameters

banners
AnimeBannerInfo[] | null
required
Array of banner items to display in the carousel. Can be null during initial loading.
currentIndex
number
required
The current active index of the carousel (0-based)
setCurrentIndex
(index: number) => void
required
Function to update the current index when navigation occurs

Return Value

bannerContainerRef
RefObject<HTMLDivElement>
React ref to attach to the scrollable container element
handleTouchStart
(e: React.TouchEvent) => void
Event handler for touch start events (mobile swiping)
handleTouchMove
(e: React.TouchEvent) => void
Event handler for touch move events (tracking swipe direction)
handleTouchEnd
() => void
Event handler for touch end events (completing the swipe)
handlePrev
() => void
Function to navigate to the previous banner
handleNext
() => void
Function to navigate to the next banner
resetInterval
() => void
Function to reset the auto-scroll timer
intervalRef
MutableRefObject<number | null>
Reference to the auto-scroll interval timer
handleScroll
(index: number) => void
Function to scroll to a specific index
handleKeyDown
(e: KeyboardEvent) => void
Event handler for keyboard navigation (arrow keys)

Usage Examples

import { useCarouselScroll } from '@hooks/useCarouselScroll'
import { useState, useEffect } from 'react'
import type { AnimeBannerInfo } from '@anime/types'

const AnimeBannerCarousel = ({ banners }: { banners: AnimeBannerInfo[] }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  
  const {
    bannerContainerRef,
    handleNext,
    handlePrev,
    handleTouchStart,
    handleTouchMove,
    handleTouchEnd,
    handleKeyDown,
    resetInterval,
  } = useCarouselScroll(banners, currentIndex, setCurrentIndex)

  useEffect(() => {
    // Set up keyboard navigation
    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [handleKeyDown])

  useEffect(() => {
    // Start auto-scroll
    resetInterval()
  }, [])

  return (
    <div className="carousel">
      <div
        ref={bannerContainerRef}
        onTouchStart={handleTouchStart}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
        className="carousel-container"
      >
        {banners.map((banner, index) => (
          <div key={banner.mal_id} className="carousel-item">
            <img src={banner.image} alt={banner.title} />
          </div>
        ))}
      </div>
      
      <button onClick={handlePrev}>Previous</button>
      <button onClick={handleNext}>Next</button>
      
      <div className="indicators">
        {banners.map((_, index) => (
          <span
            key={index}
            className={index === currentIndex ? 'active' : ''}
          />
        ))}
      </div>
    </div>
  )
}
const CustomCarousel = ({ items }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  
  const {
    bannerContainerRef,
    handleNext,
    handlePrev,
    resetInterval,
    intervalRef,
  } = useCarouselScroll(items, currentIndex, setCurrentIndex)

  // Custom auto-scroll interval (different from default 7000ms)
  useEffect(() => {
    if (intervalRef.current) clearInterval(intervalRef.current)
    
    intervalRef.current = window.setInterval(() => {
      handleNext()
    }, 5000) // 5 seconds instead of default 7

    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current)
    }
  }, [handleNext])

  return (
    <div ref={bannerContainerRef} className="carousel">
      {items.map(item => (
        <div key={item.id}>{item.content}</div>
      ))}
    </div>
  )
}
const InfiniteCarousel = ({ slides }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  
  const {
    bannerContainerRef,
    handleNext,
    handlePrev,
  } = useCarouselScroll(slides, currentIndex, setCurrentIndex)

  // The hook automatically handles looping:
  // - Going next from last item returns to first
  // - Going prev from first item goes to last

  return (
    <div>
      <div ref={bannerContainerRef} className="slides">
        {slides.map(slide => (
          <div key={slide.id}>{slide.content}</div>
        ))}
      </div>
      <button onClick={handlePrev}></button>
      <button onClick={handleNext}></button>
    </div>
  )
}
const TouchCarousel = ({ items }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  const [isSwiping, setIsSwiping] = useState(false)
  
  const {
    bannerContainerRef,
    handleTouchStart,
    handleTouchMove,
    handleTouchEnd,
  } = useCarouselScroll(items, currentIndex, setCurrentIndex)

  const onTouchStart = (e: React.TouchEvent) => {
    setIsSwiping(true)
    handleTouchStart(e)
  }

  const onTouchEnd = () => {
    setIsSwiping(false)
    handleTouchEnd()
  }

  return (
    <div
      ref={bannerContainerRef}
      onTouchStart={onTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={onTouchEnd}
      className={isSwiping ? 'swiping' : ''}
    >
      {items.map(item => (
        <div key={item.id}>{item.content}</div>
      ))}
    </div>
  )
}

Features

Touch Navigation

  • Swipe Detection: Detects left/right swipes with 30px threshold
  • Smooth Transitions: Uses native smooth scrolling behavior
  • Touch Tracking: Maintains touch start and end positions

Keyboard Navigation

  • Arrow Left: Navigate to previous banner
  • Arrow Right: Navigate to next banner
  • Accessibility: Full keyboard navigation support

Auto-Scroll

  • Default Interval: 7 seconds between automatic transitions
  • Auto-Reset: Timer resets when user manually navigates
  • Cleanup: Properly clears intervals on unmount

Infinite Loop

  • Seamless Wrapping: Automatically loops from last to first and vice versa
  • No Interruption: Smooth transitions at loop boundaries

Use Cases

  • Homepage banners showcasing featured anime
  • Image galleries with touch/keyboard navigation
  • Featured content carousels
  • Character showcases on anime detail pages
  • Episode previews in a scrollable list
  • Promotional content rotation

Configuration

Swipe Threshold

The hook uses a 30px swipe threshold by default. To detect a swipe, users must move their finger at least 30px horizontally:
// In the hook implementation:
if (deltaX > 30) {
  handleNext() // Swipe left
} else if (deltaX < -30) {
  handlePrev() // Swipe right
}

Auto-Scroll Interval

Default auto-scroll interval is 7000ms (7 seconds):
intervalRef.current = window.setInterval(() => {
  handleNext()
}, 7000)
The auto-scroll interval automatically resets whenever the user manually navigates using buttons, touch gestures, or keyboard controls. This provides a better user experience by giving users time to interact with the current item.

Accessibility

  • ✅ Full keyboard navigation support
  • ✅ Touch gesture support for mobile users
  • ✅ Mouse click support via navigation buttons
  • ✅ Programmatic scroll control
  • ✅ Proper cleanup of event listeners and timers
Always provide visible navigation buttons in addition to touch/keyboard controls for maximum accessibility:
<button onClick={handlePrev} aria-label="Previous slide"></button>
<button onClick={handleNext} aria-label="Next slide"></button>

Performance Considerations

  • Uses useCallback to memoize event handlers
  • Properly cleans up intervals and event listeners
  • Smooth scrolling uses native browser optimization
  • Touch events are efficiently tracked with refs

Source

Location: src/domains/anime/hooks/useCarouselScroll.ts:33

Build docs developers (and LLMs) love