Frontend Development 16 min read

Implementing an Adaptive Menu Bar in Vue with a Dynamic "More" Button

This article describes how to build a reusable Vue component that automatically moves overflow menu items into a "More" dropdown, calculates the cutoff point based on container width, and updates the layout on resize, providing a smooth and responsive navigation experience.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing an Adaptive Menu Bar in Vue with a Dynamic "More" Button

Background

Our company's navigation menu is dynamically configurable; some pages have many menu items while others have few. When the number of items exceeds the available width we initially used a horizontal scrollbar, which the client complained about as ugly.

Technical Solution

Base Component Development

We created a reusable Vue component AdaptiveMenuBar.vue that renders the menu area and a "More" button when items overflow.

<template>
    <div class="adaptive-menu-bar"></div>
</template>

<style lang="less" scoped>
.adaptive-menu-bar{
    width: 100%;
    height: 48px;
    background: gainsboro;
    display: flex;
    position: relative;
    overflow: hidden;
}
</style>

We added mock data for demonstration.

<template>
    <div class="adaptive-menu-bar">
        <div class="origin-menu-item-wrap">
            <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
                {{ item.name }}
            </div>
        </div>
        <div>更多</div>
    </div>
</template>

<script setup>
const menuOriginData = [
    { name: '哆啦a梦', id: 1 },
    { name: '宇智波佐助', id: 1 },
    { name: '香蕉之王奥德彪', id: 1 },
    { name: '漩涡鸣人', id: 1 },
    { name: '雏田', id: 1 },
    { name: '大雄', id: 1 },
    { name: '源静香', id: 1 },
    { name: '骨川小夫', id: 1 },
    { name: '超级马里奥', id: 1 },
    { name: '自来也', id: 1 },
    { name: '孙悟空', id: 1 },
    { name: '卡卡罗特', id: 1 },
    { name: '万年老二贝吉塔', id: 1 },
    { name: '小泽玛丽', id: 1 }
];
</script>

Implementation Idea

We calculate which menu items exceed the visible area by comparing the scrollWidth of the container with its clientWidth . Items beyond the cutoff are moved into the "More" dropdown.

More Button Display Logic

The button appears only when the container's scrollWidth is greater than its clientWidth .

<template>
  <div ref="menuBarRef" class="origin-menu-item-wrap">
      <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
        <m-button type="default" size="small">{{ item.name }}</m-button>
      </div>
  </div>
</template>
<script setup>
const menuOriginData = [
    { name: '哆啦a梦', id: 1 },
    { name: '宇智波佐助', id: 1 },
    { name: '香蕉之王奥德彪', id: 1 },
    { name: '漩涡鸣人', id: 1 },
    { name: '雏田', id: 1 },
    { name: '大雄', id: 1 },
    { name: '源静香', id: 1 },
    { name: '骨川小夫', id: 1 },
    { name: '超级马里奥', id: 1 },
    { name: '自来也', id: 1 },
    { name: '孙悟空', id: 1 },
    { name: '卡卡罗特', id: 1 },
    { name: '万年老二贝吉塔', id: 1 },
    { name: '小泽玛丽', id: 1 }
];
/* 是否展示更多按钮 */
const showMoreBtn = ref(false);

onMounted(() => {
  const menuWrapDom = menuBarRef.value;
  if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
    showMoreBtn.value = true;
  }
});
</script>

Cutoff Position Calculation

We sum the widths of .menu-item elements until the total exceeds the container width, then slice the data array at that index.

<template>
  <div ref="menuBarRef" class="origin-menu-item-wrap">
      <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
        <m-button type="default" size="small">{{ item.name }}</m-button>
      </div>
  </div>
</template>
<script setup>
const menuOriginData = [
    { name: '哆啦a梦', id: 1 },
    { name: '宇智波佐助', id: 1 },
    { name: '香蕉之王奥德彪', id: 1 },
    { name: '漩涡鸣人', id: 1 },
    { name: '雏田', id: 1 },
    { name: '大雄', id: 1 },
    { name: '源静香', id: 1 },
    { name: '骨川小夫', id: 1 },
    { name: '超级马里奥', id: 1 },
    { name: '自来也', id: 1 },
    { name: '孙悟空', id: 1 },
    { name: '卡卡罗特', id: 1 },
    { name: '万年老二贝吉塔', id: 1 },
    { name: '小泽玛丽', id: 1 }
];
/* 是否展示更多按钮 */
const showMoreBtn = ref(false);

onMounted(() => {
    const menuWrapDom = menuBarRef.value;
    if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
        showMoreBtn.value = true;
    }
    // 计算截断菜单的索引位置
    let sliceIndex = 0;
    const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
    const nodeArray = Array.prototype.slice.call(menuItemNodeList);
    let addWidth = 0;
    for (let i = 0; i < nodeArray.length; i++) {
        const node = nodeArray[i];
        // clientWidth不包含 margin,手动补 12px
        addWidth += node.clientWidth + 12;
        // 76 是更多按钮的宽度,需要计入
        if (addWidth + 76 > menuWrapDom.clientWidth) {
            sliceIndex = i;
            break;
        } else {
            sliceIndex = 0;
        }
    }
    // 根据 sliceIndex 更新显示的列表(后续代码省略)
});
</script>

Style Re‑arrangement

Two menu lists are rendered: the original (hidden) for measurement and the adjusted list for display, plus the "More" button that shows the overflow items.

<template>
    <div class="adaptive-menu-bar">
<div ref="menuBarRef" class="origin-menu-item-wrap">
            <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
                {{ item.name }}
            </div>
        </div>
<div v-for="(item, index) in menuList" :key="index" class="menu-item">
            {{ item.name }}
        </div>
<div v-if="showMoreBtn" class="dropdown-wrap">
            更多
<div class="menu-item-wrap">
                <div v-for="(item, index) in menuOriginData.slice(menuList.length)" :key="index">
                    {{ item.name }}
                </div>
            </div>
        </div>
    </div>
</template>

Code Implementation

Basic Functionality

On window resize we recompute the layout to keep the menu responsive.

const menuOriginData = [
    { name: '哆啦a梦', id: 1 },
    { name: '宇智波佐助', id: 1 },
    { name: '香蕉之王奥德彪', id: 1 },
    { name: '漩涡鸣人', id: 1 },
    { name: '雏田', id: 1 },
    { name: '大雄', id: 1 },
    { name: '源静香', id: 1 },
    { name: '骨川小夫', id: 1 },
    { name: '超级马里奥', id: 1 },
    { name: '自来也', id: 1 },
    { name: '孙悟空', id: 1 },
    { name: '卡卡罗特', id: 1 },
    { name: '万年老二贝吉塔', id: 1 },
    { name: '小泽玛丽', id: 1 }
];

// 是否展示更多按钮
const showMoreBtn = ref(false);

const setHeaderStyle = () => {
    const menuWrapDom = menuBarRef.value;
    if (!menuWrapDom) return;
    if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
        showMoreBtn.value = true;
    } else {
        showMoreBtn.value = false;
    }
    const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
    if (menuItemNodeList) {
        let addWidth = 0,
            sliceIndex = 0;
        const nodeArray = Array.prototype.slice.call(menuItemNodeList);
        for (let i = 0; i < nodeArray.length; i++) {
            const node = nodeArray[i];
            addWidth += node.clientWidth + 12;
            if (addWidth + 64 + 12 > menuWrapDom.clientWidth) {
                sliceIndex = i;
                break;
            } else {
                sliceIndex = 0;
            }
        }
        if (sliceIndex > 0) {
            menuList.value = menuOriginData.slice(0, sliceIndex);
        } else {
            menuList.value = menuOriginData;
        }
    }
};

window.addEventListener('resize', () => setHeaderStyle());

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

Full Component Code

The complete component (template, script, style) is provided without third‑party UI dependencies.

<template>
    <div class="adaptive-menu-bar">
        <!-- 原始渲染的菜单栏 -->
        <div ref="menuBarRef" class="origin-menu-item-wrap">
            <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item">
                {{ item.name }}
            </div>
        </div>

        <!-- 计算优化显示的菜单栏 -->
        <div v-for="(item, index) in menuList" :key="index" class="menu-item">
            {{ item.name }}
        </div>

        <!-- 更多按钮 -->
        <div v-if="showMoreBtn" class="dropdown-wrap">
            更多
            <!-- 更多里面的菜单 -->
            <div class="menu-item-wrap">
                <div v-for="(item, index) in menuOriginData.slice(menuList.length)" :key="index">
                    {{ item.name }}
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
import { IconMeriComponentArrowDown } from 'meri-icon';

const menuBarRef = ref();
const open = ref(false);

const menuOriginData = [
    { name: '哆啦a梦', id: 1 },
    { name: '宇智波佐助', id: 1 },
    { name: '香蕉之王奥德彪', id: 1 },
    { name: '漩涡鸣人', id: 1 },
    { name: '雏田', id: 1 },
    { name: '大雄', id: 1 },
    { name: '源静香', id: 1 },
    { name: '骨川小夫', id: 1 },
    { name: '超级马里奥', id: 1 },
    { name: '自来也', id: 1 },
    { name: '孙悟空', id: 1 },
    { name: '卡卡罗特', id: 1 },
    { name: '万年老二贝吉塔', id: 1 },
    { name: '小泽玛丽', id: 1 }
];

const menuList = ref(menuOriginData);
const showMoreBtn = ref(false);

const setHeaderStyle = () => {
    const menuWrapDom = menuBarRef.value;
    if (!menuWrapDom) return;
    if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) {
        showMoreBtn.value = true;
    } else {
        showMoreBtn.value = false;
    }
    const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
    if (menuItemNodeList) {
        let addWidth = 0,
            sliceIndex = 0;
        const nodeArray = Array.prototype.slice.call(menuItemNodeList);
        for (let i = 0; i < nodeArray.length; i++) {
            const node = nodeArray[i];
            addWidth += node.clientWidth + 12;
            if (addWidth + 64 + 12 > menuWrapDom.clientWidth) {
                sliceIndex = i;
                break;
            } else {
                sliceIndex = 0;
            }
        }
        if (sliceIndex > 0) {
            menuList.value = menuOriginData.slice(0, sliceIndex);
        } else {
            menuList.value = menuOriginData;
        }
    }
};

window.addEventListener('resize', () => setHeaderStyle());

onMounted(() => {
    setHeaderStyle();
});
</script>

<style lang="less" scoped>
.adaptive-menu-bar {
    width: 100%;
    height: 48px;
    background: gainsboro;
    display: flex;
    position: relative;
    align-items: center;
    overflow: hidden;
    .origin-menu-item-wrap {
        width: 100%;
        display: flex;
        position: absolute;
        top: 49px;
        left: 0;
        right: 0;
        bottom: 0;
        height: 48px;
        z-index: 9;
    }
    .menu-item {
        margin-left: 12px;
    }
    .dropdown-wrap {
        width: 64px;
        display: flex;
        align-items: center;
        cursor: pointer;
        justify-content: center;
        height: 28px;
        background: #fff;
        border-radius: 4px;
        overflow: hidden;
        border: 1px solid #c4c9cf;
        margin-left: 12px;
        .icon {
            width: 16px;
            height: 16px;
            margin-left: 4px;
        }
    }
}
</style>

Result

The adaptive menu bar works smoothly; overflow items are neatly placed under a "More" button, providing a clean and responsive navigation experience.

frontendjavascriptVueResponsiveDesignAdaptiveMenuBarUIComponent
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.