设计系统中bem的正确落地关键在于block必须对应可交付原子组件(如button.vue),类名需与storybook、props文档、token严格对齐,禁止硬套命名而忽略可检索性、可组合性与发布流程。

设计系统里硬套 BEM 模板反而会拖慢落地节奏——关键不是写对 block__element--modifier,而是让每个类名在组件、文档、工具链中能被一致识别、可检索、可组合。
Design System 的 Block 必须对应可交付的原子组件
设计系统里的 button、input-field、status-badge 才是合法 Block;form-layout 或 grid-container 这类名字即使带了双下划线,也不算合格 Block,因为它们不封装行为、不提供 API、不单独发布 npm 包。
- 判断标准:这个 Block 是否有独立的 Storybook 页面、Props 文档、无障碍测试用例、主题变量映射表
- 常见错误:把
card__header当作独立 Block 导出,结果发现它根本没法脱离card单独渲染或测试 - 正确做法:所有 Block 对应一个 React/Vue 组件文件(如
Button.vue),其根元素 class 必须是button,内部结构严格限定在button__text、button__icon等一级 Element
Modifier 值必须与设计令牌(Design Token)对齐,不能自由发挥
button--primary 能用,button--blue 不能进设计系统主干——前者绑定 color-primary 令牌,后者直接写死值,换品牌色时就得全局搜改 JS 和 CSS 两处。
- Modifier 名必须可枚举:比如尺寸只允许
--size-small/--size-medium/--size-large,对应设计系统定义的spacing-2/spacing-4/spacing-6 - 禁止状态类暴露实现:
button--is-loading❌,应为button--loading;JS 控制类存在与否,而非靠布尔值命名 - Element 级 Modifier 仅限局部影响:
button__icon--hidden合理,但button--loading__icon语法非法且语义混乱
SCSS 文件结构必须与组件边界完全重合
每个 Block 对应唯一 SCSS 文件,如 _button.scss,且该文件里只出现 .button、.button__*、.button--*,绝不能出现 .modal__close 或 .card__button。
- 禁止跨 Block 引用:哪怕
button和icon都是原子组件,也不能在_button.scss里写.icon样式 - 修饰符变量统一注入:用
@use 'tokens' as t,所有--primary对应t.$color-primary,避免硬编码 - 构建时自动校验:配置
stylelint-selector-bem-pattern+postcss-bem,CI 中拦截.button .icon这类后代选择器
第三方组件和 Utility Class 必须设隔离层
设计系统不能要求业务方放弃 react-datepicker 或 tailwind 工具类,但必须明确交界规则。
- 封装第三方组件时,用 wrapper Block 包一层:
date-picker-field,其内部 DOM 不暴露原生类名,只输出date-picker-field__input等标准路径 - Utility 类(如
u-text-center)只能用于微调布局,不能参与组件逻辑;date-picker-field__input本身必须完整定义字体、尺寸、边框,不依赖u-font-sm - 文档中明确标注哪些类名“可穿透”(如
button__icon允许业务方加animate-spin),哪些“不可覆盖”(如button--disabled的指针事件必须锁定)
最易被忽略的是命名与发布流程的耦合:一个 badge Block 在设计系统里通过了评审、写了文档、进了 Storybook,才允许生成 badge、badge__text、badge--status-error 这组类名;没走完这套流程就手写 user-card__badge,等于绕过设计系统做私有扩展——后续升级、主题切换、a11y 审查全都会断链。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











