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-
15:34