Skip to content

List Virtual Scrolling Component

This component renders only the DOM for visible content, reducing DOM operations and optimizing performance. It is ideal for scenarios involving large lists of data.

📌Platform Differences

APP(vue)H5WeChat Mini ProgramAlipay Mini Program

⚠️Notes

Notes

  • containerHeight must be set; otherwise, all data will be loaded and virtual scrolling optimization will not take effect
  • itemHeight must match the actual content height; otherwise, the virtual scrolling calculation will be incorrect
  • The WeChat Mini Program has some restrictions on the use of slot — named slot cannot be used multiple times within a v-for
  • For large data scenarios (e.g., 1000+ items), virtual scrolling is recommended for better performance

🏯Basic Usage Examples

Basic List

html
<template>
    <hy-list :list="list" container-height="100vh">
        <template #content="{ record }">
            <view class="list-item">
                <text>{{ record }}</text>
            </view>
        </template>
    </hy-list>
</template>

<script setup>
    import { ref } from 'vue'

    const list = ref<string[]>([])
    for (let i = 0; i < 2000; i++) {
        list.value.push(`List item--${i}`)
    }
</script>

<style scoped>
    .list-item {
        padding: 20rpx;
        font-size: 28rpx;
    }
</style>

Single-Column Data List

html
<template>
    <hy-list
        :list="userList"
        container-height="600rpx"
        item-height="120rpx"
        border
        @click="handleClick"
    >
        <template #content="{ record }">
            <view class="user-item">
                <view class="avatar" :style="{ backgroundColor: record.color }">
                    <text>{{ record.name.charAt(0) }}</text>
                </view>
                <view class="user-info">
                    <text class="user-name">{{ record.name }}</text>
                    <text class="user-desc">{{ record.desc }}</text>
                </view>
            </view>
        </template>
    </hy-list>
</template>

<script setup>
    import { ref } from 'vue';

    const userList = ref([
        { id: 1, name: 'Zhang San', desc: 'Frontend Engineer', color: '#4F8EF7' },
        { id: 2, name: 'Li Si', desc: 'UI Designer', color: '#F74F8E' },
        { id: 3, name: 'Wang Wu', desc: 'Product Manager', color: '#8EF74F' },
        { id: 4, name: 'Zhao Liu', desc: 'Backend Developer', color: '#F7C54F' },
    ]);

    const handleClick = (item) => {
        uni.showToast({
            title: `Clicked ${item.name}`,
            icon: 'none',
        });
    };
</script>

<style scoped>
    .user-item {
        display: flex;
        align-items: center;
        padding: 20rpx;
    }

    .avatar {
        width: 80rpx;
        height: 80rpx;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-size: 32rpx;
        font-weight: 600;
    }

    .user-info {
        margin-left: 20rpx;
        flex: 1;
    }

    .user-name {
        font-size: 30rpx;
        font-weight: 600;
        display: block;
    }

    .user-desc {
        font-size: 24rpx;
        color: #999;
        margin-top: 8rpx;
    }
</style>

Two-Column Product List

html
<template>
    <hy-list
        :list="goodsList"
        container-height="100vh"
        item-height="400rpx"
        :line="2"
        border
        :load="loadStatus"
        @scrollToLower="loadMore"
    >
        <!-- #ifdef H5 || APP_PLUS -->
        <template #left="{ record }">
            <view class="goods-card">
                <image :src="record.image" mode="aspectFill" class="goods-image"></image>
                <view class="goods-info">
                    <text class="goods-name">{{ record.name }}</text>
                    <text class="goods-price">¥{{ record.price }}</text>
                </view>
            </view>
        </template>
        <template #right="{ record }">
            <view class="goods-card">
                <image :src="record.image" mode="aspectFill" class="goods-image"></image>
                <view class="goods-info">
                    <text class="goods-name">{{ record.name }}</text>
                    <text class="goods-price">¥{{ record.price }}</text>
                </view>
            </view>
        </template>
        <!-- #endif -->

        <!-- #ifndef H5 || APP_PLUS -->
        <template #left-list="{ record }">
            <view class="goods-card" v-for="item in record" :key="item.id">
                <image :src="item.image" mode="aspectFill" class="goods-image"></image>
                <view class="goods-info">
                    <text class="goods-name">{{ item.name }}</text>
                    <text class="goods-price">¥{{ item.price }}</text>
                </view>
            </view>
        </template>
        <template #right-list="{ record }">
            <view class="goods-card" v-for="item in record" :key="item.id">
                <image :src="item.image" mode="aspectFill" class="goods-image"></image>
                <view class="goods-info">
                    <text class="goods-name">{{ item.name }}</text>
                    <text class="goods-price">¥{{ item.price }}</text>
                </view>
            </view>
        </template>
        <!-- #endif -->
    </hy-list>
</template>

<script setup>
    import { ref, onMounted } from 'vue';

    const goodsList = ref([]);
    const loadStatus = ref('loadMore');
    const page = ref(1);

    onMounted(() => {
        fetchData();
    });

    const fetchData = () => {
        for (let i = 0; i < 20; i++) {
            goodsList.value.push({
                id: (page.value - 1) * 20 + i,
                name: `Product ${(page.value - 1) * 20 + i + 1}`,
                price: (Math.random() * 100).toFixed(2),
                image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=product%20image%20e-commerce%20item&image_size=square',
            });
        }
    };

    const loadMore = () => {
        if (loadStatus.value === 'loading') return;
        loadStatus.value = 'loading';

        setTimeout(() => {
            if (page.value >= 5) {
                loadStatus.value = 'noMore';
                return;
            }
            page.value++;
            fetchData();
            loadStatus.value = 'loadMore';
        }, 1000);
    };
</script>

<style scoped>
    .goods-card {
        height: 100%;
        border-radius: 10rpx;
        overflow: hidden;
        background: white;
    }

    .goods-image {
        width: 100%;
        height: 280rpx;
    }

    .goods-info {
        padding: 15rpx;
    }

    .goods-name {
        font-size: 26rpx;
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .goods-price {
        font-size: 30rpx;
        color: #f44336;
        font-weight: 600;
        margin-top: 10rpx;
        display: block;
    }
</style>
html
<template>
    <hy-list :list="list" container-height="80vh" item-height="60rpx" :show-divider="false">
        <template #content="{ record }">
            <text>{{ record }}</text>
        </template>
        <template #footer>
            <view class="custom-footer">
                <hy-button type="primary" text="Load More" @click="loadMore"></hy-button>
            </view>
        </template>
    </hy-list>
</template>

<script setup>
    import { ref } from 'vue'

    const list = ref<string[]>([])
    for (let i = 0; i < 50; i++) {
        list.value.push(`List item--${i}`)
    }

    const loadMore = () => {
        uni.showToast({
            title: 'Load more',
            icon: 'none'
        })
    }
</script>

<style scoped>
    .custom-footer {
        padding: 20rpx;
        text-align: center;
    }
</style>

Styled List Items

html
<template>
    <hy-list
        :list="list"
        container-height="500rpx"
        item-height="80rpx"
        padding="20rpx"
        margin-bottom="10rpx"
        border-radius="10rpx"
        background="#f5f5f5"
        border
    >
        <template #content="{ record }">
            <text>{{ record }}</text>
        </template>
    </hy-list>
</template>

<script setup>
    import { ref } from 'vue'

    const list = ref<string[]>([])
    for (let i = 0; i < 20; i++) {
        list.value.push(`Custom style item--${i}`)
    }
</script>

API

List Props

ParameterDescriptionTypeDefault
listData listarray[]
containerHeightContainer height; must be set, otherwise all data will be loaded. Numeric values default to px unitsstring | number100%
itemHeightItem container height; must match the content height, otherwise the calculation will be incorrect. Numeric values default to px unitsstring | number40px
paddingItem container padding. Numeric values default to px unitsstring | number10
marginBottomItem container bottom margin, included in the container calculation. Numeric values default to px unitsstring | number0
borderRadiusItem container border radius. Numeric values default to px unitsstring | number3px
backgroundContainer background colorstringtransparent
borderWhether to show the borderbooleanfalse
lineNumber of columns to display (currently supports 1 and 2 columns)number1
keyFieldUnique identifier key for each itemstringid
loadLoading statusloadMore | loading | noMoreloadMore
showDividerWhether to show the bottom loading status indicatorbooleantrue

Events

Event NameDescriptionCallback Parameters
clickTriggered when a list item is clickeditem: single data item
scrollToLowerTriggered when scrolled to the bottom-

Slots

Slot NameDescriptionReceived Values
defaultCustom overall list contentrecord: data in the current visible area
contentCustom content for each item in single-column moderecord: single data item
leftSingle container on the left in two-column mode (H5/APP)record: single data item
left-listLeft list slot in two-column mode (Mini Program)record: left list data
rightSingle container on the right in two-column mode (H5/APP)record: single data item
right-listRight list slot in two-column mode (Mini Program)record: right list data
footerFooter slot for customizing the load-more area-
01:10