自定义Multi Select组件:标签超宽时动态显示“X个已选”
动态适配宽度的多选Tag折叠方案
核心思路
通过动态计算Tag总宽度与父容器可用宽度的差值,自动隐藏超出部分的Tag,替换为“X个已选”的汇总Tag。相比固定数量限制,这种方式能完美适配不同宽度的Tag和父容器。
方案一:JavaScript+ResizeObserver 精准计算(通用框架/原生JS)
步骤说明
- 计算父容器的可用宽度(需减去输入框、内边距等占用的宽度)
- 逐个累加可见Tag的宽度,同时预留“X个已选”Tag的宽度
- 当累加宽度超过可用宽度时,停止并隐藏后续Tag,显示汇总Tag
- 监听窗口resize和父容器尺寸变化,实时重新计算
原生JS代码示例
<div class="multi-select" id="multiSelect"> <div class="tags-container" id="tagsContainer"> <span class="tag">选项1</span> <span class="tag">超长选项名称示例</span> <span class="tag">选项3</span> <span class="tag">选项4</span> <span class="tag">选项5</span> </div> <span class="remaining-tag" id="remainingTag" style="display: none;"></span> <input class="select-input" placeholder="请选择"> </div> <script> const multiSelect = document.getElementById('multiSelect'); const tagsContainer = document.getElementById('tagsContainer'); const remainingTag = document.getElementById('remainingTag'); function adjustTags() { // 父容器可用宽度 = 容器总宽度 - 输入框宽度 - 内边距 const availableWidth = multiSelect.clientWidth - 60; let tags = Array.from(tagsContainer.children); let remainingCount = 0; // 先重置所有Tag为可见状态 tags.forEach(tag => tag.style.display = 'inline-block'); remainingTag.style.display = 'none'; // 动态获取汇总Tag的真实宽度(避免硬写固定值适配不同字体) remainingTag.textContent = '0个已选'; const summaryTagWidth = remainingTag.offsetWidth; // 逐步隐藏Tag直到总宽度不超出 while (tagsContainer.scrollWidth + summaryTagWidth > availableWidth && tags.length > 0) { const lastTag = tags.pop(); lastTag.style.display = 'none'; remainingCount++; } // 显示汇总Tag if (remainingCount > 0) { remainingTag.textContent = `${remainingCount}个已选`; remainingTag.style.display = 'inline-block'; } } // 初始化执行 adjustTags(); // 监听窗口大小变化 window.addEventListener('resize', adjustTags); // 监听父容器尺寸变化(比如侧边栏展开/收缩影响容器宽度) new ResizeObserver(adjustTags).observe(multiSelect); </script> <style> .multi-select { display: flex; align-items: center; width: 300px; border: 1px solid #ccc; padding: 4px; gap: 4px; } .tags-container { display: flex; gap: 4px; flex-wrap: nowrap; overflow: hidden; } .tag, .remaining-tag { background: #e0e0e0; padding: 2px 6px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; } .remaining-tag { background: #f5f5f5; } .select-input { border: none; outline: none; flex: 1; } </style>
React版本核心逻辑
import { useEffect, useRef, useState } from 'react'; const MultiSelect = ({ selectedOptions }) => { const containerRef = useRef(null); const [visibleCount, setVisibleCount] = useState(selectedOptions.length); const [remainingCount, setRemainingCount] = useState(0); useEffect(() => { const calculateVisibleTags = () => { if (!containerRef.current || selectedOptions.length === 0) { setVisibleCount(selectedOptions.length); setRemainingCount(0); return; } const container = containerRef.current; const availableWidth = container.clientWidth - 80; // 根据实际布局调整偏移量 const tags = container.querySelectorAll('.tag'); // 动态计算汇总Tag宽度 const tempSummaryTag = document.createElement('span'); tempSummaryTag.className = 'tag'; tempSummaryTag.textContent = '0个已选'; document.body.appendChild(tempSummaryTag); const summaryWidth = tempSummaryTag.offsetWidth; document.body.removeChild(tempSummaryTag); let totalWidth = summaryWidth; let count = 0; for (let i = 0; i < tags.length; i++) { const tagWidth = tags[i].offsetWidth; if (totalWidth + tagWidth > availableWidth) break; totalWidth += tagWidth; count++; } setVisibleCount(count); setRemainingCount(selectedOptions.length - count); }; calculateVisibleTags(); const observer = new ResizeObserver(calculateVisibleTags); observer.observe(containerRef.current); window.addEventListener('resize', calculateVisibleTags); return () => { observer.disconnect(); window.removeEventListener('resize', calculateVisibleTags); }; }, [selectedOptions]); return ( <div className="multi-select" ref={containerRef}> {selectedOptions.slice(0, visibleCount).map(opt => ( <span key={opt.value} className="tag"> {opt.label} <button className="tag-close">×</button> </span> ))} {remainingCount > 0 && ( <span className="tag remaining-tag">{remainingCount}个已选</span> )} <input className="select-input" placeholder="请选择..." /> </div> ); };
关键注意事项
- 避免硬写宽度:必须动态计算“X个已选”Tag的真实宽度,适配不同字体、字号和语言
- 监听容器变化:使用
ResizeObserver代替仅监听窗口resize,覆盖父容器被其他元素影响的场景 - 禁止Tag压缩:给Tag设置
flex-shrink: 0,防止flex布局压缩Tag宽度导致计算误差
内容的提问来源于stack exchange,提问作者RaySoju
相关产品推荐
相关产品推荐

