Skip to content

Index-Bar Component

An index bar component for quickly locating list content, supporting both tap and slide interactions, compatible with Mini Program and App platforms.

📌 Platform Compatibility

APP(vue)H5WeChat Mini ProgramAlipay Mini Program

🏯 Basic Usage Example

html
<template>
    <view class="index-bar-demo">
        <view class="content-list">
            <view
                v-for="(group, index) in contactList"
                :key="index"
                :id="`section-${group.index}`"
                class="contact-group"
            >
                <view class="group-title">{{ group.index }}</view>
                <view v-for="(contact, idx) in group.list" :key="idx" class="contact-item">
                    {{ contact.name }}
                </view>
            </view>
        </view>

        <!-- Index bar -->
        <hy-index-bar v-model="activeIndex" :index-list="indexList" @scroll="handleScroll" />
    </view>
</template>

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

    // Contact data
    const contactList = ref([
        { index: 'A', list: [{ name: 'A Jie' }, { name: 'A Ming' }] },
        { index: 'B', list: [{ name: 'Bei Bei' }, { name: 'Bao Bao' }] },
        // More data...
    ]);

    // Index list
    const indexList = computed(() => contactList.value.map((item) => item.index));

    // Currently active index
    const activeIndex = ref('A');

    // Scroll event handler
    const handleScroll = (index) => {
        // Scroll to the corresponding position based on the index
        uni.createSelectorQuery()
            .select(`#section-${index}`)
            .boundingClientRect((rect) => {
                if (rect) {
                    uni.pageScrollTo({
                        scrollTop: rect.top,
                        duration: 300,
                    });
                }
            })
            .exec();
    };
</script>

<style lang="scss" scoped>
    .index-bar-demo {
        position: relative;
        height: 100vh;
    }

    .content-list {
        padding: 20rpx;
    }

    .contact-group {
        margin-bottom: 40rpx;
    }

    .group-title {
        font-size: 32rpx;
        font-weight: bold;
        padding: 10rpx 0;
        background-color: #f5f5f5;
    }

    .contact-item {
        font-size: 28rpx;
        padding: 20rpx 0;
        border-bottom: 1rpx solid #eee;
    }
</style>

Hiding the Toast

html
<hy-index-bar v-model="activeIndex" :index-list="indexList" :show-toast="false" />

Custom Styling

html
<hy-index-bar
    v-model="activeIndex"
    :index-list="indexList"
    :index-color="'#666'"
    :active-index-color="'#007AFF'"
    :index-bg-color="'#f0f0f0'"
    :active-index-bg-color="'#e0e0e0'"
    :index-size="14"
    :width="30"
    :height="'80%'"
    :customStyle="{ backgroundColor: 'rgba(255, 255, 255, 0.8)' }"
/>

Index List Data Format

Two data formats are supported:

1. String Array

js
const indexList = ref(['A', 'B', 'C', 'D', 'E']);

2. Object Array

js
const indexList = ref([
  { index: 'A', data: [...] },
  { index: 'B', data: [...] },
  { index: 'C', data: [...] }
])

Complete Code

vue
<template>
    <hy-config-provider>
        <scroll-view class="hy-scroll-view" scroll-y :scroll-top="scrollTop" @scroll="handleScroll">
            <view
                v-for="item in indexList"
                :key="item.index"
                class="hy-index-section"
                :id="`index-${item.index}`"
            >
                <view class="hy-index-section__title hy-title">{{ item.index }}</view>
                <view class="hy-index-section__container">
                    <view
                        v-for="(city, cityIndex) in item.data"
                        :key="cityIndex"
                        class="hy-index-section__item"
                    >
                        {{ city }}
                    </view>
                </view>
            </view>
        </scroll-view>

        <hy-index-bar
            v-model="currentActiveIndex"
            :index-list="indexs"
            :position="position"
            :show-toast="showToast"
            @click="handleIndexClick"
            @scroll="handleIndexScroll"
        />
    </hy-config-provider>
</template>
ts
import { ref, reactive, computed, getCurrentInstance, nextTick, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { getRect, sleep } from '@hy-app/ui';

const instance = getCurrentInstance();
// Index bar position
const position = ref<string>('right');
const sectionRect = ref<UniNamespace.NodeInfo[]>([]);
const scrollTop = ref<number>(0);
// Whether to show the toast
const showToast = ref<boolean>(true);
// Currently active index
const currentActiveIndex = ref<string>('');
const isScroll = ref<boolean>(true);
// Mock city data
const cityData = reactive({
    A: ['Anqing', 'Anyang', 'Anshan', 'Anshun', 'Ankang', 'Aksu', 'Altay'],
    B: [
        'Beijing',
        'Shanghai',
        'Guangzhou',
        'Shenzhen',
        'Chongqing',
        'Chengdu',
        'Hangzhou',
        'Wuhan',
        "Xi'an",
        'Suzhou',
        'Tianjin',
        'Nanjing',
        'Changsha',
        'Zhengzhou',
        'Dongguan',
        'Qingdao',
        'Shenyang',
        'Ningbo',
        'Kunming',
    ],
    C: ['Changchun', 'Changzhou', 'Changsha', 'Chengdu', 'Chongqing', 'Fuzhou', 'Changchun', 'Changsha'],
    D: ['Dalian', 'Dongguan', 'Dezhou', 'Datong', 'Daqing', 'Dali'],
    E: ['Ezhou', 'Enshi', 'Ezhou'],
    F: ['Fuzhou', 'Foshan', 'Fushun', 'Fuxin', 'Fuyang', 'Fuzhou'],
    G: ['Guangzhou', 'Guiyang', 'Guilin', 'Ganzhou', 'Guangyuan', "Guang'an"],
    H: ['Hangzhou', 'Hefei', 'Harbin', 'Haikou', 'Hohhot', 'Huzhou', 'Handan', 'Hanzhong', 'Hengshui'],
    J: ['Jinan', 'Changchun', 'Jilin', 'Jinzhou', 'Jingzhou', 'Jingmen', 'Jinjiang'],
    K: ['Kunming', 'Kaifeng', 'Karamay', 'Kashgar'],
    L: ['Lanzhou', 'Luoyang', 'Linyi', 'Liaocheng', 'Longyan', 'Lijiang'],
    M: ['Mianyang', "Ma'anshan", 'Maoming', 'Meizhou', 'Meishan'],
    N: ['Nanjing', 'Nanchang', 'Nanning', 'Nantong', 'Ningbo', 'Nanyang', 'Nanchong'],
    P: ['Pingdingshan', 'Panjin', 'Panzhihua', 'Pingxiang'],
    Q: ['Qingdao', 'Quanzhou', 'Qujing', 'Qinhuangdao', 'Qiqihar', 'Qingyang'],
    R: ['Rizhao', 'Shigatse', 'Rongchang'],
    S: ['Shenzhen', 'Suzhou', 'Shenyang', 'Shijiazhuang', 'Shaoxing', 'Xiamen', 'Shangqiu', 'Shangrao', 'Shiyan'],
    T: ['Tianjin', 'Taiyuan', 'Tangshan', 'Taizhou', 'Taizhou', 'Tongliao', 'Turpan'],
    W: ['Wuhan', "Xi'an", 'Wenzhou', 'Wuxi', 'Weifang', 'Urumqi', 'Weihai', 'Weinan'],
    X: ["Xi'an", 'Xiamen', 'Xuzhou', 'Xinxiang', 'Xinyang', 'Xiangyang', 'Xining'],
    Y: ['Yantai', 'Yangzhou', 'Yichang', 'Yueyang', "Yan'an", 'Yinchuan', 'Yuncheng'],
    Z: ['Zhengzhou', 'Zhuzhou', 'Zhuhai', 'Zhongshan', 'Zhanjiang', 'Zhaoqing', 'Zhangjiakou', 'Zaozhuang'],
});

// Convert to index list format
const indexList = computed(() => {
    return Object.keys(cityData)
        .sort()
        .map((key) => ({
            index: key,
            title: key,
            data: cityData[key as keyof typeof cityData],
        }));
});
const indexs = computed(() => {
    return Object.keys(cityData).sort();
});

onMounted(() => {
    nextTick(() => {
        getRect('.hy-index-section', true).then((rect) => {
            sectionRect.value = rect;
        });
    });
});

// Handle scroll event
const handleScroll = (e: any) => {
    // Use uni.createSelectorQuery() to get the position info of all index sections
    const scrollTop = e.detail.scrollTop;
    if (isScroll.value) {
        const index = sectionRect.value.findIndex((item) => item.top - 1 > scrollTop) - 1;
        const keys = Object.keys(cityData);
        console.log(keys[index], '==');
        currentActiveIndex.value = keys[index];
    }
};

// Handle index click event
const handleIndexClick = (index: string, event: Event) => {
    scrollToSection(index);
};

// Handle index scroll event
const handleIndexScroll = (index: string) => {
    scrollToSection(index);
};

const scrollToSection = async (i: string) => {
    isScroll.value = false;
    const index = Object.keys(cityData).findIndex((item) => item === i);
    scrollTop.value = sectionRect.value[index]?.top!;
    await sleep();
    isScroll.value = true;
};
scss
.hy-scroll-view {
    width: 100%;
    height: 100%;
}

.hy-index-section {
    padding: $hy-border-margin-padding-base;

    @include e(title) {
    }

    @include e(item) {
        font-size: 28rpx;
        padding: $hy-border-margin-padding-base;
        border-bottom: $hy-border-line;
        &:last-child {
            border: none;
        }
    }

    @include e(container) {
        padding: $hy-border-margin-padding-base 0;
        border-radius: $hy-border-radius-base;
        background: $hy-background--container;
    }
}

API

IndexBar Props

PropDescriptionTypeDefault
v-modelCurrently active index valuestring-
indexListIndex list data; supports a string array or an object arraystring|number|IIndexItem[]-
positionIndex bar position, optional values: 'left', 'right'left|rightright
showToastWhether to show the index letter toastbooleantrue
indexColorIndex item colorstring-
activeIndexColorColor of the index item in active statestring-
indexBgColorIndex item background colorstringtransparent
activeIndexBgColorBackground color of the index item in active statestring-
indexSizeIndex item size; numbers default to pxnumber|string12
heightIndex bar height; numbers default to pxnumber|string100%
widthIndex bar width; numbers default to pxnumber/string20
customStyleCustom external styles to applyCSSProperties-
customClassCustom external class namestring-

Events

EventDescriptionCallback Parameters
clickTriggered when an index item is clickedindex: string - the clicked index value
event: Event - the event object
scrollTriggered when sliding on the index barindex: string - the index value currently slid to
03:29