用 flex: 1 + min-width: 0 实现响应式等分;::before 仅作用于非首节点并用 transform 居中画线;data-status 属性驱动三态样式;移动端改用 ::after 垂直连线。

用 display: flex 平分步骤节点,别靠 width: 25% 硬写
Flex 布局天然适合等分步骤节点,但很多人直接给每个 .step 设 width: 25%,结果响应式一缩就错位、文字换行后高度不一致、节点间距难控制。
正确做法是让父容器用 display: flex + justify-content: space-between,子项不设宽度,靠 flex: 1 均分剩余空间(注意:需配合 min-width: 0 防文字撑开):
.steps {
display: flex;
justify-content: space-between;
}
.step {
flex: 1;
min-width: 0;
text-align: center;
}这样无论 3 步还是 5 步,节点自动等距;文字再多也不会挤垮布局。
用 ::before 画连接线,但必须避开第一个节点
伪元素画线最轻量,但容易犯两个错:一是所有节点都加 ::before,导致第一个节点左边多出一根线;二是用 position: absolute 脱离文档流,造成高度塌陷或重叠。
稳妥方案是只给从第二个节点开始的每个 .step 加 ::before,并用 transform: translateX(-50%) 居中对齐:
.step:not(:first-child)::before {
content: "";
position: absolute;
top: 50%;
left: -50%;
width: 100%;
height: 2px;
background: #ccc;
transform: translateY(-50%);
}关键点:left: -50% 是为了让伪元素左边缘对准当前节点中心,width: 100% 才能刚好覆盖到上一个节点中心。
进度态要用 data-status 控制,别用 class 切换整条线
静态步骤条和动态进度条本质不同——后者需要明确“已完成”“进行中”“未开始”三态,硬切 class 容易漏掉某一段线的颜色或节点图标状态。
推荐用属性驱动:data-status="done"、data-status="active"、data-status="todo",然后用属性选择器批量控制:
.step[data-status="done"]::before,
.step[data-status="active"]::before {
background: #007bff;
}
.step[data-status="done"] .step-icon {
background: #007bff;
color: white;
}
.step[data-status="todo"] .step-icon {
background: #e9ecef;
color: #6c757d;
}这样逻辑清晰,增减步骤数也不用改 CSS 规则数量。
移动端适配时,flex-direction: column 不够,得重置伪元素定位
横屏步骤条在手机上堆成一列,光改 flex-direction: column 不行——原来的 ::before 还在水平方向画线,会变成一堆重叠的短横线。
必须用媒体查询重置伪元素行为:
@media (max-width: 768px) {
.steps {
flex-direction: column;
}
.step:not(:first-child)::before {
display: none; /* 横向线全关 */
}
.step:not(:last-child)::after {
content: "";
display: block;
width: 2px;
height: 24px;
background: #ccc;
margin: 16px auto 0;
}
.step[data-status="done"]::after,
.step[data-status="active"]::after {
background: #007bff;
}
}注意:竖向连接线用 ::after 放在每个非末尾节点下方,比复用 ::before 更可控。
真正麻烦的不是画线,而是当步骤文本长度差异大、图标尺寸不统一、或需要支持 RTL 时,transform: translateX(-50%) 的基准点会偏移;这时候得用 calc() 配合 width 显式计算偏移量,或者干脆放弃伪元素,改用 SVG 线段。


















