Scrollable is a utility component that provides a scrollable container with configurable overflow behavior and edge detection capabilities.

Importing

The component can be imported via:

import { Scrollable } from '@customerio/pluma-components/react';

Usage

The Scrollable component provides a scrollable container with configurable overflow behavior and edge detection capabilities. It's useful for creating scrollable areas that need to respond when content reaches the edges.

Scrollable content that's larger than its container
<Scrollable overflow="both" style={{ width: '200px', height: '200px' }}>
  <div style={{ width: '1000px', height: '1000px', background: 'linear-gradient(135deg, #f00, #0f0, #00f)' }}>
    Scrollable content that's larger than its container
  </div>
</Scrollable>

Overflow Direction

The overflow prop controls which direction the content can scroll:

  • 'x': Horizontal scrolling only
  • 'y': Vertical scrolling only
  • 'both': Both horizontal and vertical scrolling
  • 'hidden': No scrolling, content is clipped
Horizontal ('x')
Horizontal scrollable content
Vertical ('y')
Vertical scrollable content
Both directions ('both')
Bidirectional scrollable content
Hidden overflow ('hidden')
Content clipped when overflow is hidden
<Box display="flex" gap="200">
  <Box>
    <Box mb="100">Horizontal ('x')</Box>
    <Scrollable 
      overflow="x" 
      style={{ width: '200px', height: '100px' }}
    >
      <div style={{ width: '300%', height: '100%', background: 'linear-gradient(to right, #f00, #0f0, #00f)' }}>
        Horizontal scrollable content
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Vertical ('y')</Box>
    <Scrollable 
      overflow="y" 
      style={{ width: '200px', height: '100px' }}
    >
      <div style={{ width: '100%', height: '300%', background: 'linear-gradient(to bottom, #f00, #0f0, #00f)' }}>
        Vertical scrollable content
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Both directions ('both')</Box>
    <Scrollable 
      overflow="both" 
      style={{ width: '200px', height: '100px' }}
    >
      <div style={{ width: '300%', height: '300%', background: 'linear-gradient(135deg, #f00, #0f0, #00f)' }}>
        Bidirectional scrollable content
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Hidden overflow ('hidden')</Box>
    <Scrollable 
      overflow="hidden" 
      style={{ width: '200px', height: '100px' }}
    >
      <div style={{ width: '300%', height: '300%', background: 'linear-gradient(135deg, #f00, #0f0, #00f)' }}>
        Content clipped when overflow is hidden
      </div>
    </Scrollable>
  </Box>
</Box>

Edge Detection

The Scrollable component provides callbacks that fire when content touches or leaves any edge (top, right, bottom, left):

  • onTouchEdge: Called when content touches an edge
  • onLeaveEdge: Called when content leaves an edge

These callbacks are useful for implementing custom scroll behaviors, UI indicators, and infinite scrolling.

Edge Threshold

You can configure how close to an edge the content needs to be to trigger the callbacks using the edgeThreshold prop. This value represents the distance in pixels from the edge:

Default threshold (1px)
Scroll to see edge detection with default 1px threshold
Large threshold (50px)
Scroll to see edge detection with 50px threshold - triggers earlier
<Box display="flex" gap="200">
  <Box>
    <Box mb="100">Default threshold (1px)</Box>
    <Scrollable 
      overflow="y" 
      style={{ width: '200px', height: '150px', border: '1px solid #ccc' }}
      onTouchEdge={(side) => console.log('Touched:', side)}
      onLeaveEdge={(side) => console.log('Left:', side)}
    >
      <div style={{ height: '300px', padding: '10px', background: 'linear-gradient(to bottom, #f0f0f0, #e0e0e0)' }}>
        Scroll to see edge detection with default 1px threshold
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Large threshold (50px)</Box>
    <Scrollable 
      overflow="y" 
      edgeThreshold={50}
      style={{ width: '200px', height: '150px', border: '1px solid #ccc' }}
      onTouchEdge={(side) => console.log('Touched:', side)}
      onLeaveEdge={(side) => console.log('Left:', side)}
    >
      <div style={{ height: '300px', padding: '10px', background: 'linear-gradient(to bottom, #f0f0f0, #e0e0e0)' }}>
        Scroll to see edge detection with 50px threshold - triggers earlier
      </div>
    </Scrollable>
  </Box>
</Box>

Edge Indicators

The withEdgeIndicators prop enables visual fade effects at the edges to indicate when more content is available for scrolling. It uses CSS mask-image to fade the actual content pixels at scroll edges, creating a clean visual cue without z-index issues or overlaying gradients.

When Edge Indicators Appear

Edge indicators are shown when:

  • withEdgeIndicators is true
  • Content is scrollable in that direction
  • The scroll position is not at the edge (based on edgeThreshold)
  • Only works with single-direction overflow ('x' or 'y', not 'both')
Vertical with Edge Indicators

This content is taller than the container.

Scroll to see fade indicators at top/bottom when not at the edges.

More content...
Bottom content
Horizontal with Edge Indicators
This content is wider than the container. Scroll horizontally to see left/right edge indicators.
Both Directions (No Indicators)
When overflow="both", edge indicators are disabled to avoid conflicts.
<Box display="flex" gap="200">
  <Box>
    <Box mb="100">Vertical with Edge Indicators</Box>
    <Scrollable 
      overflow="y" 
      withEdgeIndicators={true}
      style={{ width: '200px', height: '150px', border: '1px solid #ccc' }}
    >
      <div style={{ height: '400px', padding: '20px', background: 'linear-gradient(to bottom, #f0f8ff, #e6f3ff, #ddeeff)' }}>
        <p>This content is taller than the container.</p>
        <p>Scroll to see fade indicators at top/bottom when not at the edges.</p>
        <div style={{ marginTop: '100px' }}>More content...</div>
        <div style={{ marginTop: '100px' }}>Bottom content</div>
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Horizontal with Edge Indicators</Box>
    <Scrollable 
      overflow="x" 
      withEdgeIndicators={true}
      style={{ width: '200px', height: '150px', border: '1px solid #ccc' }}
    >
      <div style={{ width: '400px', height: '130px', padding: '20px', background: 'linear-gradient(to right, #fff8f0, #fff3e6, #ffeedd)', whiteSpace: 'nowrap' }}>
        This content is wider than the container. Scroll horizontally to see left/right edge indicators.
      </div>
    </Scrollable>
  </Box>

  <Box>
    <Box mb="100">Both Directions (No Indicators)</Box>
    <Scrollable 
      overflow="both" 
      withEdgeIndicators={true}
      style={{ width: '200px', height: '150px', border: '1px solid #ccc' }}
    >
      <div style={{ width: '400px', height: '300px', padding: '20px', background: 'linear-gradient(135deg, #f0f0f0, #e0e0e0)' }}>
        When overflow="both", edge indicators are disabled to avoid conflicts.
      </div>
    </Scrollable>
  </Box>
</Box>

API

Distance threshold in pixels from edge to trigger onTouchEdge/onLeaveEdge callbacks

Callback fired when the scrollable content leaves an edge

Callback fired when the scrollable content touches an edge

Controls the overflow behavior of the component

Whether to show edge fade indicators at the edges when content is scrollable but not currently at that edge. Uses CSS mask-image to fade the actual content pixels at scroll edges.

The fade size defaults to 48px and can be overridden via the CSS variable exposed in scrollable.vars.fadeSize.