ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

data-status 属性 + CSS属性选择器:告别状态类名地狱

data-status 属性 + CSS属性选择器:告别状态类名地狱 最近在改一个老的表单组件被按钮的状态切换折腾到差点掀桌loading、success、error 三种状态原来代码里全是classList.add/classList.remove到处增删类名。加一个新状态要同时改 JS 逻辑、改 CSS 选择器、改 HTML 结构三处联动漏掉一处样式就鬼畜。痛定思痛之后我把状态全部收敛到>button classbtn is-loading is-success提交/button这种写法的问题在于loading、success、error这些概念是状态不是样式类。今天可能是蓝底白字的 loading明天产品经理说 loading 要用灰底转圈你只需要改 CSS但如果你把状态直接做成 class 名往往就会碰到改样式发现还有别的地方依赖同一个 class的窘境。把状态放到>button classbtn>div classorder-card>.order-card { border-left: 4px solid transparent; } .order-card[data-statuspending] { border-left-color: #faad14; } .order-card[data-statuspaid] { border-left-color: #52c41a; } .order-card[data-statusshipped] { border-left-color: #1677ff; } .order-card[data-statuscompleted] { border-left-color: #8c8c8c; }这里还能配合伪类做状态和内容的联动。比如待支付状态给个金色的圆点提示已支付状态加一个对勾背景都能靠[data-status]控制。再强调一个经验一个元素的>.btn[data-statuserror] { /* 内部状态样式 */ }这样优先级至少是 (0, 2, 0)比使用者单独一个类名要高。3. 三个实战场景表单、步骤条与主题切换3.1 表单按钮状态loading/success/error按钮是>button classsubmit-btn>.submit-btn[data-statusloading] { pointer-events: none; opacity: .7; } .submit-btn[data-statusloading] .btn-text::before { content: ; display: inline-block; width: 14px; height: 14px; margin-right: 8px; border: 2px solid rgba(255,255,255,.4); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; } .submit-btn[data-statussuccess] { background: #52c41a; } .submit-btn[data-statuserror] { background: #ff4d4f; } keyframes spin { to { transform: rotate(360deg); } }这只是基础版。更进一步可以把涟漪光圈扩散效果做成状态反馈当按钮从 loading 切到 success 时加一个::after伪元素的扩散动画给用户一个完成了的视觉脉冲。CSS 大概长这样.submit-btn[data-statussuccess]::after { content: ; position: absolute; inset: 0; border-radius: inherit; background: rgba(82,196,26,.5); animation: ripple 1s ease-out; } keyframes ripple { from { transform: scale(1); opacity: 1; } to { transform: scale(1.4); opacity: 0; } }注意这里动画只在状态切换的一瞬间触发。如果你连续点击触发两次 success动画可能没法重播这时候 JS 层面需要重置状态为 idle 再切回 success或者直接移除节点重挂具体看你的场景。3.2 步骤条completed/active/disabled后台系统里步骤条很常见第 1 步已完成、第 2 步进行中、第 3 步未开始。每个步骤节点就是一个带状态的元素ol classsteps li classstep>.step[data-statuscompleted] .step-index { background: #52c41a; border-color: #52c41a; } .step[data-statusactive] .step-index { color: #1677ff; border-color: #1677ff; font-weight: 600; } .step[data-statusdisabled] .step-index { color: #bbb; border-color: #ddd; }步骤条的状态还有一个特点状态是递增的很少会跳变。比如从active切到completed或者从disabled切到active。这种连续性状态非常适合用>.step[data-statuscompleted] .step::before { background: #52c41a; } .step[data-statusdisabled] .step::before { background: #e5e5e5; }用相邻兄弟选择器纯 CSS 就能实现连线颜色跟着前一个节点状态走这个写法很推荐。3.3 主题切换data-theme 与>div classtheme-card>.theme-card[data-statusselected] { outline: 2px solid #1677ff; } .theme-card[data-statusselected][data-themedark] { background: #1f1f1f; } .theme-card[data-statusselected][data-themelight] { background: #fff; }两个属性一组合选择器语义非常清晰。脱离 JS 逻辑也能轻松看出当前选中了哪个主题卡片。顺便说一个现代 CSS 的进阶点用:has()做父级状态联动。比如整个页面容器根据是否有某个推送状态来自动调整样式.layout:has(.toast[data-statuserror]) .content-area { filter: grayscale(.3); }这个场景比较小但能说明组合之后的选择器表达能力很强。:has()现在主流浏览器都支持了用之前查一下兼容性即可。4. JS 更新>.submit-btn { transition: background-color .3s, opacity .3s, box-shadow .3s; } .submit-btn[data-statussuccess] { background-color: #52c41a; box-shadow: 0 0 12px rgba(82,196,26,.5); }数字加载动画、涟漪光圈这类效果本质上都是基于keyframes动画不占用transition。上面 3.1 已经给了例子。这里再补充一个状态相关的字体渐变和流光边框玩法把成功状态的文本做出渐变效果让用户一眼看出状态变了.submit-btn[data-statussuccess] .btn-text { background: linear-gradient(90deg, #fff, #b7eb8f, #fff); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: shimmer 2s linear infinite; }这类视觉效果很抓眼球但要注意克制。状态的主旨是传达信息动画只是辅助别让用户盯着按钮转三秒还看不出是成功还是失败。4.3 用事件监听替代主动判断当一个页面上有多个元素的状态需要联动时手动逐个改>stepsEl.addEventListener(click, (e) { const step e.target.closest(.step); if (!step || step.dataset.status ! disabled) return; stepsEl.querySelectorAll(.step).forEach((s) s.setAttribute(data-status, disabled)); step.setAttribute(data-status, active); });这样的好处是状态流转逻辑集中在一个函数里DOM 上的状态值只是最终结果。配合>button classbtn btn--primary>button classsubmit-btn>function updateStatus(el, status) { el.setAttribute(data-status, status); if (status loading) { el.setAttribute(aria-busy, true); } else { el.removeAttribute(aria-busy); } }很多人觉得这是额外负担但做过一轮无障碍改造之后你就会发现>button classbtn px-4 py-2 font-semibold>.btn { --btn-bg: #1677ff; background: var(--btn-bg); } .btn[data-statuserror] { --btn-bg: #ff4d4f; }这样状态切换只需要改一个变量后面的 background、box-shadow、border-color 都会跟着变代码瘦身很明显。6. 踩坑清单data-status 样式不生效的常见原因6.1 属性值大小写和匹配问题>const normalizeStatus (s) s.toLowerCase();另一个细节是属性值里的空格。>supports selector(:has(*)) { .layout:has(.toast[data-statuserror]) .content-area { filter: grayscale(.3); } }另外用构建工具压缩 CSS 时属性选择器本身不会被破坏但如果值里带了特殊字符比如>
返回列表