---
name: "ScrollView"
mode: "component"
versionRequirements:
  - { name: "@ray-js/ray", version: "0.5.10" }
title: "ScrollView - Scroll container"
---

## ScrollView

> [VERSION] @ray-js/ray >= 0.5.10

### Description

Scrollable view container that supports horizontal or vertical scrolling, with configurable pull-to-refresh, scroll event listeners, and other features.

### Props

| Property | Type | Required | Default | Since | Description |
| --- | --- | --- | --- | --- | --- |
| `scrollX` | `boolean` | No | `false` | - | Enable horizontal scrolling |
| `scrollY` | `boolean` | No | `false` | - | Enable vertical scrolling |
| `upperThreshold` | `number \| string` | No | `50` | - | Distance from the top/left at which the scrolltoupper event fires |
| `lowerThreshold` | `number \| string` | No | `50` | - | Distance from the bottom/right at which the scrolltolower event fires |
| `scrollTop` | `number \| string` | No | `0` | - | Set vertical scroll position |
| `scrollLeft` | `number \| string` | No | `0` | - | Set horizontal scroll position |
| `scrollIntoView` | `string` | No | - | - | The value should be the id of a child element (id must not start with a digit). The element will be scrolled into view along the enabled scrolling direction |
| `scrollIntoViewOffset` | `number` | No | `0` | `1.7.58` | Additional offset, in px, when scrolling to the scrollIntoView target node |
| `scrollWithAnimation` | `boolean` | No | `false` | - | Use animated transition when setting the scroll position |
| `onScroll` | `(event: ScrollEvent) => void` | No | - | - | Fires on scroll |
| `onScrollToUpper` | `(event: ScrolltoupperEvent) => void` | No | - | - | Fires when scrolled to the top/left |
| `onScrollToLower` | `(event: ScrolltolowerEvent) => void` | No | - | - | Fires when scrolled to the bottom/right |
| `refresherEnabled` | `boolean` | No | `false` | `0.9.3` | Enable custom pull-to-refresh |
| `refresherThreshold` | `number` | No | `45` | `0.9.3` | Set the custom pull-to-refresh threshold |
| `refresherDefaultStyle` | `"black" \| "white" \| "none"` | No | `"black"` | `0.9.3` | Set the default style for custom pull-to-refresh. Supports black, white, and none; none disables the default style |
| `refresherBackground` | `string` | No | `"#FFF"` | `0.9.3` | Set the background color of the custom pull-to-refresh area |
| `refresherTriggered` | `boolean` | No | `false` | `0.9.3` | Set the current pull-to-refresh state: true means pull-to-refresh has been triggered; false means it has not |
| `hideScrollbar` | `boolean` | No | `true` | - | Hide the scrollbar |
| `bounces` | `boolean` | No | `true` | `1.7.58` | Whether to enable the iOS scroll bounce effect (fully supported on iOS 16.0+) |
| `onRefresherpulling` | `(event: RefresherPullingEvent) => void` | No | - | `0.9.3` | Fires when the custom pull-to-refresh control is pulled |
| `onRefresherrefresh` | `(event: RefresherRefreshEvent) => void` | No | - | `0.9.3` | Fires when custom pull-to-refresh is triggered |
| `onRefresherrestore` | `(event: RefresherRestoreEvent) => void` | No | - | `0.9.3` | Fires when custom pull-to-refresh is reset |
| `onRefresherabort` | `(event: RefresherAbortEvent) => void` | No | - | `0.9.3` | Triggered when custom pull-to-refresh is aborted |

### Referenced Types

##### `interface` ScrollEvent

| Property | Type | Description |
| --- | --- | --- |
| `detail` | `ScrollDetail` | Scroll event detail data |

##### `interface` ScrolltoupperEvent

| Property | Type | Description |
| --- | --- | --- |
| `detail` | `ScrollDirectionDetail` | Scroll event detail data |

##### `interface` ScrolltolowerEvent

| Property | Type | Description |
| --- | --- | --- |
| `detail` | `ScrollDirectionDetail` | Scroll event detail data |

##### `interface` RefresherPullingEvent

```typescript
export interface RefresherPullingEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherpulling';
}
```

##### `interface` RefresherRefreshEvent

```typescript
export interface RefresherRefreshEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherrefresh';
}
```

##### `interface` RefresherRestoreEvent

```typescript
export interface RefresherRestoreEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherrestore';
}
```

##### `interface` RefresherAbortEvent

```typescript
export interface RefresherAbortEvent extends BaseEvent {
  /** 事件类型 */
  type: 'refresherabort';
}
```

##### `interface` ScrollDetail

| Property | Type | Description |
| --- | --- | --- |
| `scrollLeft` | `number` | Horizontal scroll position |
| `scrollTop` | `number` | Vertical scroll position |
| `scrollHeight` | `number` | Scroll content height |
| `scrollWidth` | `number` | Scroll content width |
| `deltaX` | `number` | Horizontal scroll delta |
| `deltaY` | `number` | Vertical scroll delta |

##### `interface` BaseEvent

| Property | Type | Description |
| --- | --- | --- |
| `type` | `string` | Event type |
| `timeStamp` | `number` | Time in milliseconds since the page opened |
| `target` | `Target` | Source component of the event |
| `currentTarget` | `Target` | Collection of property values for the current component |
| `mark` | `any` | Event mark data |

##### `interface` ScrollDirectionDetail

| Property | Type | Description |
| --- | --- | --- |
| `direction` | `"top" \| "left" \| "bottom" \| "right"` | Scroll direction |

##### `interface` Target

| Property | Type | Description |
| --- | --- | --- |
| `id` | `string` | id of the event source component |
| `dataset` | `Record<string, unknown>` | Collection of custom attributes from the `dataset` on the event source component |


### Examples

#### Basic usage

```tsx
import React from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function BasicScrollView() {
  const handleScrollToUpper = () => {
    console.log('Scrolled to top');
  };

  const handleScrollToLower = () => {
    console.log('Scrolled to bottom');
  };

  return (
    <ScrollView
      scrollY
      style={{ height: '400rpx', backgroundColor: '#f5f5f5' }}
      onScrollToUpper={handleScrollToUpper}
      onScrollToLower={handleScrollToLower}
    >
      {Array.from({ length: 10 }, (_, i) => (
        <View
          key={i}
          style={{
            height: '100rpx',
            margin: '10rpx',
            backgroundColor: '#fff',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text>List item {i + 1}</Text>
        </View>
      ))}
    </ScrollView>
  );
}
```

#### Pull to refresh

```tsx
import React, { useState, useCallback } from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function PullToRefreshDemo() {
  const [list, setList] = useState(() =>
    Array.from({ length: 10 }, (_, i) => `Initial data ${i + 1}`)
  );
  const [refreshing, setRefreshing] = useState(false);
  const [loading, setLoading] = useState(false);

  const loadData = useCallback((startIndex: number) => {
    return new Promise<string[]>((resolve) => {
      setTimeout(() => {
        const newList = Array.from(
          { length: 10 },
          (_, i) => `Data ${startIndex + i + 1}`
        );
        resolve(newList);
      }, 1000);
    });
  }, []);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      const newList = await loadData(-1);
      setList(prevList => [...newList, ...prevList]);
    } finally {
      setRefreshing(false);
    }
  }, [loadData]);

  const handleScrollToLower = useCallback(async () => {
    if (loading) return;
    setLoading(true);
    try {
      const newList = await loadData(list.length);
      setList(prevList => [...prevList, ...newList]);
    } finally {
      setLoading(false);
    }
  }, [loading, list.length, loadData]);

  return (
    <ScrollView
      scrollY
      style={{ height: '600rpx', backgroundColor: '#f5f5f5' }}
      refresherEnabled
      refresherThreshold={50}
      refresherTriggered={refreshing}
      refresherBackground="#f5f5f5"
      onRefresherrefresh={handleRefresh}
      onScrollToLower={handleScrollToLower}
      lowerThreshold={100}
    >
      {list.map((item, index) => (
        <View
          key={index}
          style={{
            height: '100rpx',
            margin: '10rpx 20rpx',
            backgroundColor: '#fff',
            borderRadius: '8rpx',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text>{item}</Text>
        </View>
      ))}
      {loading && (
        <View
          style={{
            height: '80rpx',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: '#999', fontSize: '28rpx' }}>Loading...</Text>
        </View>
      )}
    </ScrollView>
  );
}
```

#### Horizontal scrolling

```tsx
import React from 'react';
import { ScrollView, View, Text } from '@ray-js/ray';

export default function HorizontalScrollDemo() {
  const cards = ['Card 1', 'Card 2', 'Card 3', 'Card 4', 'Card 5'];

  return (
    <ScrollView
      scrollX
      style={{
        width: '100%',
        height: '200rpx',
        whiteSpace: 'nowrap',
      }}
      onScroll={(e) => console.log('Horizontal scroll position:', e.detail.scrollLeft)}
    >
      {cards.map((card, index) => (
        <View
          key={index}
          style={{
            width: '300rpx',
            height: '180rpx',
            marginRight: '20rpx',
            backgroundColor: '#1890ff',
            borderRadius: '12rpx',
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Text style={{ color: '#fff', fontSize: '32rpx' }}>{card}</Text>
        </View>
      ))}
    </ScrollView>
  );
}
```

#### Scroll to a specific position

```tsx
import React, { useState } from 'react';
import { ScrollView, View, Text, Button } from '@ray-js/ray';

export default function ScrollIntoViewDemo() {
  const [targetId, setTargetId] = useState('');
  const sections = ['section-a', 'section-b', 'section-c', 'section-d'];

  return (
    <View>
      <View style={{ display: 'flex', marginBottom: '20rpx' }}>
        {sections.map((id) => (
          <Button
            key={id}
            size="mini"
            style={{ marginRight: '10rpx' }}
            onClick={() => setTargetId(id)}
          >
            Scroll to {id.split('-')[1].toUpperCase()}
          </Button>
        ))}
      </View>
      <ScrollView
        scrollY
        scrollWithAnimation
        scrollIntoView={targetId}
        scrollIntoViewOffset={10}
        style={{ height: '400rpx', backgroundColor: '#f5f5f5' }}
      >
        {sections.map((id, index) => (
          <View
            key={id}
            id={id}
            style={{
              height: '300rpx',
              margin: '10rpx',
              backgroundColor: ['#e6f7ff', '#fff7e6', '#f6ffed', '#fff1f0'][index],
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
          >
            <Text style={{ fontSize: '36rpx' }}>Section {id.split('-')[1].toUpperCase()}</Text>
          </View>
        ))}
      </ScrollView>
    </View>
  );
}
```


### FAQ

#### Why can't scroll-view scroll inside a popup extension component?

Add the `disableScroll` attribute to the popup component and set it to `false` to enable scrolling.

#### How do I listen for scroll-view scrolling to the bottom?

You can handle this directly in the `onScroll` method, or use `onScrollToLower` to listen for the scroll height of `scrollView` to determine whether it has scrolled to the bottom.
`scrollHeight` is the total height of all `View` elements inside `scrollView`, and `scrollTop` is the scroll offset value.
