You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Multi Select组件:标签超宽时动态显示“X个已选”

动态适配宽度的多选Tag折叠方案

核心思路

通过动态计算Tag总宽度与父容器可用宽度的差值,自动隐藏超出部分的Tag,替换为“X个已选”的汇总Tag。相比固定数量限制,这种方式能完美适配不同宽度的Tag和父容器。


方案一:JavaScript+ResizeObserver 精准计算(通用框架/原生JS)

步骤说明

  1. 计算父容器的可用宽度(需减去输入框、内边距等占用的宽度)
  2. 逐个累加可见Tag的宽度,同时预留“X个已选”Tag的宽度
  3. 当累加宽度超过可用宽度时,停止并隐藏后续Tag,显示汇总Tag
  4. 监听窗口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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 01:07:40