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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.