---
name: "scroll-view"
mode: "component"
versionRequirements:
  - { name: "Base Library", version: "2.10.0" }
title: "scroll-view - Scrollable view area supporting horizontal and vertical scrolling"
---

## scroll-view

> [VERSION] Base Library >= 2.10.0

### Description

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

### Props

| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `refresher-enabled` | `boolean` | No | `false` | Enable custom pull-to-refresh |
| `refresher-threshold` | `number` | No | `45` | Set the custom pull-down refresh threshold, in px |
| `refresher-default-style` | `string` | No | `"black"` | Set the default style for custom pull-to-refresh; none means no default style |
| `refresher-background` | `string` | No | `"#fff"` | Set the background color of the custom pull-to-refresh area |
| `refresher-triggered` | `boolean` | No | `false` | Set the current pull-to-refresh state: true means triggered; false means not triggered |
| `scroll-x` | `boolean` | No | `false` | Allow horizontal scrolling |
| `scroll-y` | `boolean` | No | `false` | Allow vertical scrolling |
| `upper-threshold` | `number` | No | `50` | Distance from the top/left at which the scrolltoupper event fires, in px |
| `lower-threshold` | `number` | No | `50` | Distance from the bottom/right at which the scrolltolower event fires, in px |
| `scroll-top` | `number` | No | `0` | Set vertical scroll position, in px |
| `scroll-left` | `number` | No | `0` | Set horizontal scroll position, in px |
| `scroll-into-view` | `string` | No | - | The value should be the id of a child element (id cannot start with a digit). It scrolls to that element along the enabled scroll direction |
| `scroll-into-view-offset` | `number` | No | `0` | Additional offset when scrolling to the scroll-into-view target node, in px |
| `scroll-with-animation` | `boolean` | No | `false` | Use animation when setting the scroll position |
| `hide-scrollbar` | `boolean` | No | `true` | Whether to hide the scrollbar |
| `bounces` | `boolean` | No | `true` | Whether to enable the iOS scroll bounce effect (fully supported on iOS 16.0+) |

#### Events

| Event | Type | Description |
| --- | --- | --- |
| `scroll` | `(event: ScrollEvent) => void` | Triggered on scroll |
| `scrolltoupper` | `(event: ScrolltoupperEvent) => void` | Triggered when scrolled to the top/left |
| `scrolltolower` | `(event: ScrolltolowerEvent) => void` | Triggered when scrolled to the bottom/right |
| `refresherpulling` | `(event: Object) => void` | Triggered when the custom pull-to-refresh control is pulled down |
| `refresherrefresh` | `(event: Object) => void` | Triggered when custom pull-to-refresh is activated |
| `refresherrestore` | `(event: Object) => void` | Triggered when custom pull-to-refresh is reset |
| `refresherabort` | `(event: Object) => void` | Triggered when custom pull-to-refresh is aborted |

**ScrollEvent**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"scroll"` | Event type |
| `detail` | `ScrollDetail` | Scroll event detail data |
| `scrollLeft` | `number` | Horizontal scroll position |
| `scrollTop` | `number` | Vertical scroll position |
| `scrollHeight` | `number` | Scrollable content height |
| `scrollWidth` | `number` | Scrollable content width |
| `deltaX` | `number` | Horizontal scroll delta |
| `deltaY` | `number` | Vertical scroll delta |

**ScrolltoupperEvent**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"scrolltoupper"` | Event type |
| `detail` | `ScrollDirectionDetail` | Scroll event detail data |

**ScrolltolowerEvent**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"scrolltolower"` | Event type |
| `detail` | `ScrollDirectionDetail` | Scroll event detail data |

**refresherpulling callback object**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"refresherpulling"` |  |

**refresherrefresh callback object**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"refresherrefresh"` |  |

**refresherrestore callback object**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"refresherrestore"` |  |

**refresherabort callback object**

| Field | Type | Description |
| --- | --- | --- |
| `type` | `"refresherabort"` |  |

### Examples

#### Basic usage

*index.tyml*

```xml
<scroll-view
  scroll-y="{{true}}"
  style="height: 400rpx;"
  class="scroll-container"
  upper-threshold="{{50}}"
  lower-threshold="{{50}}"
  bind:scrolltoupper="onScrollToUpper"
  bind:scrolltolower="onScrollToLower"
  bind:scroll="onScroll"
>
  <view ty:for="{{list}}" ty:key="*this" class="scroll-item">
    <text>{{item}}</text>
  </view>
</scroll-view>
```

*index.tyss*

```css
.scroll-container {
  background-color: #f5f5f5;
}
.scroll-item {
  height: 100rpx;
  margin: 10rpx;
  background-color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
}
```

*index.js*

```javascript
Page({
  data: {
    list: Array.from({ length: 10 }, (_, i) => 'List item ' + (i + 1)),
  },
  onScrollToUpper() {
    console.log('Scrolled to top');
  },
  onScrollToLower() {
    console.log('Scrolled to bottom');
  },
  onScroll(e) {
    console.log('scrollTop:', e.detail.scrollTop);
  },
});
```

#### Pull-to-refresh

*index.tyml*

```xml
// index.tyml
<scroll-view
  scroll-y="{{true}}"
  style="height: 600rpx;"
  class="scroll-container"
  refresher-enabled="{{true}}"
  refresher-threshold="{{50}}"
  refresher-triggered="{{refreshing}}"
  refresher-background="#f5f5f5"
  bind:refresherrefresh="onRefresh"
  bind:scrolltolower="onLoadMore"
  lower-threshold="{{100}}"
>
  <view ty:for="{{list}}" ty:key="*this" class="scroll-item">
    <text>{{item}}</text>
  </view>
  <view ty:if="{{loading}}" class="loading-tip">
    <text>Loading...</text>
  </view>
</scroll-view>
```

*index.tyss*

```css
.scroll-container {
  background-color: #f5f5f5;
}
.scroll-item {
  height: 100rpx;
  margin: 10rpx 20rpx;
  background-color: #fff;
  border-radius: 8rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}
.loading-tip {
  height: 80rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #999;
  font-size: 28rpx;
}
```

*index.js*

```javascript
Page({
  data: {
    list: Array.from({ length: 10 }, (_, i) => 'Initial data ' + (i + 1)),
    refreshing: false,
    loading: false,
  },
  onRefresh() {
    this.setData({ refreshing: true });
    setTimeout(() => {
      var newList = Array.from({ length: 5 }, (_, i) => 'Refreshed data ' + (i + 1));
      this.setData({ list: newList.concat(this.data.list), refreshing: false });
    }, 1000);
  },
  onLoadMore() {
    if (this.data.loading) return;
    this.setData({ loading: true });
    var len = this.data.list.length;
    setTimeout(() => {
      var more = Array.from({ length: 5 }, (_, i) => 'Data ' + (len + i + 1));
      this.setData({ list: this.data.list.concat(more), loading: false });
    }, 1000);
  },
});
```

#### Horizontal scrolling

*index.tyml*

```xml
<scroll-view
  scroll-x="{{true}}"
  class="horizontal-scroll"
  bind:scroll="onScroll"
>
  <view class="card-list">
    <view ty:for="{{cards}}" ty:key="*this" class="card">
      <text class="card-text">{{item}}</text>
    </view>
  </view>
</scroll-view>
```

*index.tyss*

```css
.horizontal-scroll {
  width: 100%;
  height: 200rpx;
  white-space: nowrap;
}
.card-list {
  display: inline-flex;
}
.card {
  width: 300rpx;
  height: 180rpx;
  margin-right: 20rpx;
  background-color: #1890ff;
  border-radius: 12rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}
.card-text {
  color: #fff;
  font-size: 32rpx;
}
```

*index.js*

```javascript
Page({
  data: {
    cards: ['Card 1', 'Card 2', 'Card 3', 'Card 4', 'Card 5'],
  },
  onScroll(e) {
    console.log('Horizontal scroll position:', e.detail.scrollLeft);
  },
});
```

#### Scroll to a specific element

*index.tyml*

```xml
// index.tyml
<view class="btn-row">
  <button
    ty:for="{{anchors}}"
    ty:key="*this"
    size="mini"
    bindtap="scrollTo"
    data-id="{{item}}"
  >Go to {{item}}</button>
</view>
<scroll-view
  scroll-y="{{true}}"
  scroll-into-view="{{targetId}}"
  scroll-with-animation="{{true}}"
  hide-scrollbar="{{false}}"
  style="height: 400rpx;"
  class="scroll-container"
>
  <view
    ty:for="{{anchors}}"
    ty:key="*this"
    id="{{item}}"
    class="section"
  >
    <text>{{item}}</text>
  </view>
</scroll-view>
```

*index.tyss*

```css
.btn-row {
  display: flex;
  gap: 10rpx;
  padding: 20rpx;
}
.scroll-container {
  background-color: #f5f5f5;
}
.section {
  height: 300rpx;
  margin: 10rpx 20rpx;
  background-color: #fff;
  border-radius: 8rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32rpx;
}
```

*index.js*

```javascript
Page({
  data: {
    anchors: ['sec-a', 'sec-b', 'sec-c', 'sec-d'],
    targetId: '',
  },
  scrollTo(e) {
    this.setData({ targetId: e.currentTarget.dataset.id });
  },
});
```


### 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 `bind:scroll` method, or use `bind:scrolltolower` to listen to the scroll height of `scroll-view` to determine if it has scrolled to the bottom.
`scrollHeight` is the total height of all `view` elements inside `scroll-view`, and `scrollTop` is the current scroll value.

#### When a custom page mask is used, the content under the mask also scrolls when scrolling inside the mask?

Add a `catch:touchmove` event to the `scroll-view` or `view` inside the mask to prevent event bubbling.
