在餐厅菜单、课程大纲、目录页这类排版场景里,经常能看到这样一种经典样式:条目名称贴着左侧,价格或页码贴着右侧,两者之间用一条虚线连接。视觉上虚线像是“自动撑开”的,无论名称多长、容器多宽,虚线总能恰好填满中间的空隙。不少人在第一次实现这个效果时会卡住,比如用一串句号模拟虚线、用绝对定位画线,结果换一个菜名就全乱了。其实用 flex 布局来做,思路非常清晰,而且扩展性很好。

核心思路:flex 的自动分配与弹性伸缩
flex 布局天然适合这种“两端内容 + 中间填充”的结构。我们先把每一行菜单拆成三个部分:左侧的菜名、中间的虚线、右侧的价格。容器设置 display: flex 之后,关键在于让中间的虚线元素具备两个特性:第一,它默认被两端的内容挤压到剩余空间里;第二,当剩余空间变大或变小时,它能够跟着伸缩。
具体做法是给中间元素设置 flex: 1(等价于 flex-grow: 1; flex-shrink: 1; flex-basis: 0%;),表示它不占据固定宽度,而是瓜分容器中剩余的所有空间。菜名和价格保持自然宽度,虚线就自动填满了中间的空隙。虚线本身可以通过 border-bottom 来画,一行 HTML 加几行 CSS 就能出效果。
<ul class="menu">
<li class="menu-item">
<span class="name">红烧肉</span>
<span class="dots"></span>
<span class="price">38 元</span>
</li>
<li class="menu-item">
<span class="name">黑椒牛柳意面配香煎鸡胸</span>
<span class="dots"></span>
<span class="price">58 元</span>
</li>
</ul>
.menu {
list-style: none;
padding: 0;
width: 420px;
}
.menu-item {
display: flex; /* 关键:启用弹性布局 */
align-items: baseline;/* 让文字基线对齐,虚线位置更自然 */
margin-bottom: 12px;
}
.name {
flex-shrink: 0; /* 菜名过长时也不被压缩 */
}
.dots {
flex: 1; /* 瓜分剩余空间,自动撑开 */
border-bottom: 1px dotted #999;
margin: 0 8px; /* 与两侧文字留一点呼吸感 */
}
.price {
flex-shrink: 0;
color: #c0392b;
font-weight: bold;
}这里有几个细节值得注意。align-items: baseline 让菜名和价格按文字基线对齐,即使两者字号不同也不会上下错位;而虚线元素本身没有文字,会以最后一个人的基线作为参照,配合 border-bottom 恰好落在文字底部附近。如果发现虚线位置偏高或偏低,可以给 .dots 加一点 transform: translateY(3px) 微调。
虚线的三种画法对比:哪种更适合你
用 border-bottom: 1px dotted 是最简单的画法,但它的缺点是虚线间距不可控,且不同浏览器渲染的圆点大小略有差异。如果设计稿对虚线样式有明确要求,比如要求实线段加间距的 dashed 效果,或者要求虚线更细、颜色更淡,就需要考虑其他方案。
第二种方案是使用 repeating-linear-gradient 背景绘制。这种方式自由度最高,可以精确控制每一段的长度、间距和粗细,适合对视觉还原度要求高的项目。写法是在中间元素上铺一层水平方向重复的渐变背景,并通过 background-size 控制高度,让“线”贴在文字底部。
.dots {
flex: 1;
height: 1em; /* 给背景一个占位高度 */
background-image: repeating-linear-gradient(
to right,
#bbb 0,
#bbb 4px, /* 实线段宽 4px */
transparent 4px,
transparent 10px /* 间距 6px */
);
background-size: 100% 1px; /* 线只有 1px 高 */
background-position: 0 bottom; /* 贴在元素底部 */
background-repeat: no-repeat;
margin: 0 8px;
}第三种方案是伪元素加 border-top,思路和第一种类似,好处是不需要额外的 <span> 占位,但代价是 HTML 结构不够语义化时不好复用。三种方案总结如下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| border-bottom: dotted | 代码最少,兼容性极好 | 间距不可控,浏览器渲染略有差异 |
| repeating-linear-gradient | 样式完全可控,可做任意虚实比例 | 写法稍复杂,低版本浏览器不支持 |
| 伪元素 + border | 不增加 DOM 节点 | 结构耦合,多行文本时不好处理 |
进阶问题:超长菜名与多行文本怎么办
真实的菜单里总有几个特别长的菜名,比如“黑松露野菌炖老鸡汤配竹荪”。如果不做处理,长名称会把价格挤出容器,或者把虚线压得完全消失。解决方案分两层:首先给菜名设置 flex-shrink: 0 防止被压缩,同时给整个条目设置 min-width: 0 避免 flex 子项的默认最小宽度问题;其次,当菜名确实超出容器宽度时,允许它换行,此时虚线元素会自动退化为一个很小的点甚至消失,视觉上依然成立。
如果希望长菜名换行后虚线只出现在最后一行文字后面,可以用另一种思路:把菜名和虚线放进同一个左对齐的容器,让虚线跟随内容流式排布。不过这种做法复杂度较高,大多数菜单场景下,直接让虚线在名称和价格之间自适应就足够了。还有一种偷懒但有效的兜底手段:给菜名加 text-overflow: ellipsis 和 max-width,超长时显示省略号,保证单行排版永远不塌。
.name {
flex-shrink: 0;
max-width: 65%; /* 限制最大占比 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; /* 超长显示省略号 */
}
.menu-item {
min-width: 0; /* 修复 flex 子项默认最小宽度 */
}最后提一个容易被忽略的细节:如果价格列希望严格右对齐成一列,而不是跟随各自的菜名浮动,可以把多行菜单改成表格布局或者给 .price 设置固定宽度并右对齐。flex 布局的优势在于“每行独立自适应”,而表格布局的优势在于“跨行列对齐”,两者各有适用场景,按需求选择即可。掌握这套“两端固定、中间弹性”的模式后,目录、账单明细、套餐说明等类似排版都能直接套用。