1.BEM
是什么?
BEM (Block Element Modifier) 是一种前端 CSS 类名命名方法论,由俄罗斯 Yandex 团队提出。它通过特定的命名规则来组织代码,提高代码的可维护性和可复用性。
核心概念
BEM 将页面拆分为三个主要部分:Block(块)/ Element(元素) / Modifier(修饰符)
1. Block(块)
- 独立的页面组件/模块
- 具有独立功能和意义的实体
- 示例:
header
,menu
,button
,search-form
js
<!-- Block: 按钮 -->
<button class="button">Click me</button>
<!-- Block: 导航菜单 -->
<nav class="menu">
<ul>
<li>Home</li>
<li>About</li>
</ul>
</nav>
//对应css
.button { /* 样式 */ }
.menu { /* 样式 */ }
2. Element(元素)
- 块的组成部分,没有独立意义
- 必须依附于某个块存在
- 通过双下划线
__
与块连接 - 示例:
menu__item
,button__icon
html
<!-- Block: 按钮,Element: 图标 -->
<button class="button">
<span class="button__icon">🔔</span>
Click me
</button>
<!-- Block: 菜单,Element: 菜单项 -->
<nav class="menu">
<ul>
<li class="menu__item">Home</li>
<li class="menu__item">About</li>
</ul>
</nav>
//对应css
<style>
.button__icon { /* 按钮内的图标样式 */ }
.menu__item { /* 菜单中的每一项样式 */ }
</style>
3. Modifier(修饰符)
-
定义块或元素的外观/行为状态
-
通过双连字符
--
连接 -
示例:
button--primary
,menu--vertical
,button__icon--large
js
<!-- Block: 按钮,Modifier: 主色调 -->
<button class="button button--primary">Primary Button</button>
<!-- Block: 按钮,Modifier: 禁用状态 -->
<button class="button button--disabled">Disabled</button>
<!-- Block: 菜单,Modifier: 垂直布局 -->
<nav class="menu menu--vertical">
<!-- ... -->
</nav>
<!-- element: 菜单,Modifier: 大小 -->
<span class="button__icon button__icon--large">XXX</span>
//对应css
<style>
.button--primary { /* 块主题样式 */ }
.button--disabled { /* 块禁用状态 */ }
.menu--vertical { /* 块的垂直状态 */ }
.button__icon--large { font-size: 20px; }/* 按钮图标元素的大小 */
</style>
具体例子
html
<!-- Block: 卡片 -->
<div class="card">
<!-- Element: 卡片标题 -->
<h2 class="card__title">Welcome</h2>
<!-- Element: 卡片内容 -->
<p class="card__content">This is a card component.</p>
<!-- Element: 按钮(属于 Card 的一部分,也可以是独立 Block) -->
<button class="button button--primary">Click Me</button>
</div>
//对应bem样式
<style>
.card {
border: 1px solid #ccc;
padding: 20px;
}
.card__title {
font-size: 24px;
font-weight: bold;
}
.card__content {
margin: 10px 0;
}
.button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.button--primary {
background-color: blue;
color: white;
}
</style>
2.基于BEM实现elementPlus组件
2.1 el-input
一般组件库都会多加一个前缀,不影响bem的规则,如下
go
`el-`(表示 Element 生态相关组件)
`app-`(表示主应用模块)
`ui-`(表示通用 UI 组件)
- el-input 为 block
- el-input--large 为 Modifier
- el-input--small 为 Modifier
- el-input__wrapper 为 element
- el-input__label 为 element
- el-input__inner 为 element
组件定义
js
<template>
<br>
<div :class="['el-input',`el-input--${size}`]">
<div class="el-input__wrapper">
<label v-if="label" class="el-input__label" >
{{ label }}
</label>
<input
:placeholder="placeholder"
:disabled="disabled"
:size="size"
class="el-input__inner"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'
defineOptions({
name: 'ElInput'
})
interface Props {
modelValue: string
label?: string
placeholder?: string
disabled?: boolean
size?: 'large' | 'default' | 'small' // 支持 size
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请输入内容',
disabled: false,
size: 'default', // 默认尺寸
})
</script>
<style scoped>
/* ===== BEM 命名规范样式(前缀 el-)===== */
.el-input {
width: 100%;
line-height:normal;
}
/* 带标签的输入框外层容器 */
.el-input__wrapper {
display: flex;
flex-direction: row;
gap: 8px;
}
/* 默认尺寸的标签 */
.el-input__label {
font-size: 14px;
color: #606266;
font-weight: 500;
min-width: fit-content;
}
/* 默认尺寸的 wrapper 容器间距 */
.el-input__wrapper {
gap: 8px;
}
/* ===== 尺寸变体:large ===== */
.el-input--large {
line-height: 24px;
}
.el-input--large .el-input__label {
font-size: 18px;
font-weight: 500;
}
/* ===== 尺寸变体:small ===== */
.el-input--small {
line-height: 14px;
}
.el-input--small .el-input__label {
font-size: 12px;
font-weight: 500;
}
.el-input__inner {
background-color: #f5f7fa;
border-color: #e4e7ed;
color: #c0c4cc;
}
.el-input--large .el-input__inner {
line-height: 1.5;
}
.el-input--small .el-input__inner {
line-height: 0.9;
}
</style>
使用
html
<el-input
label="用户名(小号)"
placeholder="紧凑布局"
size="small"
/>
<el-input
label="用户名(大号)"
placeholder="宽松布局"
size="large"
/>

2.2 el-button
- el-button 为 block
- el-button--primary 为 Modifier
html
<template>
<button class="el-button">默认按钮</button>
<button class="el-button el-button--primary">主要按钮</button>
<button class="el-button el-button--success">成功按钮</button>
<button class="el-button el-button--danger">危险按钮</button>
<button class="el-button el-button--disabled " disabled>禁用按钮</button>
<!-- 不同尺寸 -->
<button class="el-button el-button--small">小按钮</button>
<button class="el-button el-button--large">大按钮</button>
</template>
<script setup lang="ts">
defineOptions({
name: 'ElButton2'
})
</script>
<style lang="scss">
// ====== 设计 Token(类似 Element Plus 的设计变量)======
$--color-white: #ffffff;
$--color-primary: #409eff;
$--color-success: #67c23a;
$--color-danger: #f56c6c;
$--color-warning: #e6a23c;
$--button-padding-vertical: 12px;
$--button-padding-horizontal: 24px;
$--button-font-size: 14px;
$--button-border-radius: 4px;
$--button-small-padding-vertical: 8px;
$--button-small-padding-horizontal: 16px;
$--button-small-font-size: 12px;
$--button-large-padding-vertical: 16px;
$--button-large-padding-horizontal: 32px;
$--button-large-font-size: 16px;
// ========== Button Block ==========
.el-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: $--button-border-radius;
font-size: $--button-font-size;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-align: center;
outline: none;
background: #fff;
color: #606266;
padding: $--button-padding-vertical $--button-padding-horizontal;
// 禁用状态 Modifier
&.el-button--disabled {
opacity: 0.6;
cursor: not-allowed;
background: #f5f7fa;
color: #c0c4cc;
border-color: #e4e7ed;
}
// 基础变体:primary
&.el-button--primary {
background-color: $--color-primary;
color: $--color-white;
border-color: $--color-primary;
&:hover {
background-color: darken($--color-primary, 10%);
border-color: darken($--color-primary, 10%);
}
}
// 基础变体:success
&.el-button--success {
background-color: $--color-success;
color: $--color-white;
border-color: $--color-success;
&:hover {
background-color: darken($--color-success, 10%);
border-color: darken($--color-success, 10%);
}
}
// 基础变体:danger
&.el-button--danger {
background-color: $--color-danger;
color: $--color-white;
border-color: $--color-danger;
&:hover {
background-color: darken($--color-danger, 10%);
border-color: darken($--color-danger, 10%);
}
}
// ========== 尺寸 Modifier ==========
// 小尺寸
&.el-button--small {
font-size: $--button-small-font-size;
padding: $--button-small-padding-vertical $--button-small-padding-horizontal;
}
// 大尺寸
&.el-button--large {
font-size: $--button-large-font-size;
padding: $--button-large-padding-vertical $--button-large-padding-horizontal;
}
}
</style>

sass优化
使用sass需要了解部分api 参考
封装
js
@mixin button-variant($background, $color, $border) {
background-color: $background;
color: $color;
border-color: $border;
&:hover {
background-color: darken($background, 10%);
border-color: darken($border, 10%);
}
}
使用
js
.el-button {
// ... 基础样式同上
&.el-button--primary {
@include button-variant($--color-primary, $--color-white, $--color-primary);
}
&.el-button--success {
@include button-variant($--color-success, $--color-white, $--color-success);
}
&.el-button--danger {
@include button-variant($--color-danger, $--color-white, $--color-danger);
}
}
封装成组件
styles/mixin.scss 定义统一的全局样式变量
css
$namespace: 'el';
$element-separator: '__';
$modifier-separator: '--';
$state-prefix: 'is-';
$--header-padding: 0 20px !default;
$--footer-padding: 0 20px !default;
$--main-padding: 20px !default;
//color
$--color-white: #ffffff !default;
$--color-text-regular: #606266 !default;
$--color-text-placeholder: #c0c4cc !default;
$--border-color-base: #dcdfe6 !default;
$--button-default-border-color: $--border-color-base !default;
/* Color
-------------------------- */
/// color|1|Brand Color|0
$--color-primary: #409eff !default;
/// color|1|Background Color|4
$--color-white: #ffffff !default;
/// color|1|Background Color|4
$--color-black: #000000 !default;
/// color|1|Functional Color|1
$--color-success: #67c23a !default;
/// color|1|Functional Color|1
$--color-warning: #e6a23c !default;
/// color|1|Functional Color|1
$--color-danger: #f56c6c !default;
/// color|1|Functional Color|1
$--color-info: #909399 !default;
$--color-success-light: mix($--color-white, $--color-success, 80%) !default;
$--color-warning-light: mix($--color-white, $--color-warning, 80%) !default;
$--color-danger-light: mix($--color-white, $--color-danger, 80%) !default;
$--color-info-light: mix($--color-white, $--color-info, 80%) !default;
$--color-success-lighter: mix($--color-white, $--color-success, 90%) !default;
$--color-warning-lighter: mix($--color-white, $--color-warning, 90%) !default;
$--color-danger-lighter: mix($--color-white, $--color-danger, 90%) !default;
$--color-info-lighter: mix($--color-white, $--color-info, 90%) !default;
/// color|1|Font Color|2
$--color-text-primary: #303133 !default;
/// color|1|Font Color|2
$--color-text-regular: #606266 !default;
/// color|1|Font Color|2
$--color-text-secondary: #909399 !default;
/// color|1|Font Color|2
$--color-text-placeholder: #c0c4cc !default;
/// color|1|Border Color|3
$--border-color-base: #dcdfe6 !default;
/// color|1|Border Color|3
$--border-color-light: #e4e7ed !default;
/// color|1|Border Color|3
$--border-color-lighter: #ebeef5 !default;
/// color|1|Border Color|3
$--border-color-extra-light: #f2f6fc !default;
// Background
/// color|1|Background Color|4
$--background-color-base: #f5f7fa !default;
// size
$--font-size-base: 14px !default;
$--font-weight-primary: 500 !default;
// border
$--border-radius-base: 4px !default;
$--border-width-base: 1px !default;
$--border-style-base: solid !default;
$--border-color-hover: $--color-text-placeholder !default;
$--border-base: $--border-width-base $--border-style-base $--border-color-base !default;
//button
$--button-font-size: $--font-size-base !default;
$--button-font-weight: $--font-weight-primary !default;
$--button-primary-font-color: $--color-white !default;
$--button-default-background-color: $--color-white !default;
$--button-default-font-color: $--color-text-regular !default;
$--button-padding-vertical: 12px !default;
$--button-padding-horizontal: 20px !default;
$--button-border-radius: $--border-radius-base !default;
$--button-primary-border-color: $--color-primary !default;
$--button-primary-font-color: $--color-white !default;
$--button-primary-background-color: $--color-primary !default;
$--button-success-border-color: $--color-success !default;
$--button-success-font-color: $--color-white !default;
$--button-success-background-color: $--color-success !default;
$--button-warning-border-color: $--color-warning !default;
$--button-warning-font-color: $--color-white !default;
$--button-warning-background-color: $--color-warning !default;
$--button-danger-border-color: $--color-danger !default;
$--button-danger-font-color: $--color-white !default;
$--button-danger-background-color: $--color-danger !default;
$--button-active-shade-percent:10% !default;
$--button-medium-font-size: $--font-size-base !default;
/// borderRadius||Border|2
$--button-medium-border-radius: $--border-radius-base !default;
/// padding||Spacing|3
$--button-medium-padding-vertical: 10px !default;
/// padding||Spacing|3
$--button-medium-padding-horizontal: 20px !default;
/// fontSize||Font|1
$--button-small-font-size: 12px !default;
$--button-small-border-radius: #{$--border-radius-base - 1} !default;
/// padding||Spacing|3
$--button-small-padding-vertical: 9px !default;
/// padding||Spacing|3
$--button-small-padding-horizontal: 15px !default;
/// fontSize||Font|1
$--button-large-font-size: 12px !default;
$--button-large-border-radius: #{$--border-radius-base - 1} !default;
/// padding||Spacing|3
$--button-large-padding-vertical: 15px !default;
/// padding||Spacing|3
$--button-large-padding-horizontal: 25px !default;
// bem
@mixin b($block) {
$B: $namespace + '-' + $block !global;
.#{$B} {
@content;
}
}
// 添加ben后缀 el-button-state size啥的
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector +
& +
$modifier-separator +
$unit +
','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
@mixin button-size(
$padding-vertical,
$padding-horizontal,
$font-size,
$border-radius
) {
padding: $padding-vertical $padding-horizontal;
font-size: $font-size;
border-radius: $border-radius;
&.is-round {
padding: $padding-vertical $padding-horizontal;
}
}
@mixin button-variant($color, $background-color, $border-color) {
color: $color;
background-color: $background-color;
border-color: $border-color;
&:hover,
&:focus {
background: mix(
$--color-white,
$background-color,
20%
);
border-color: mix(
$--color-white,
$border-color,
20%
);
color: $color;
}
&:active {
background: mix(
$--color-black,
$background-color,
$--button-active-shade-percent
);
border-color: mix(
$--color-black,
$border-color,
$--button-active-shade-percent
);
color: $color;
outline: none;
}
&.is-active {
background: mix(
$--color-black,
$background-color,
$--button-active-shade-percent
);
border-color: mix(
$--color-black,
$border-color,
$--button-active-shade-percent
);
color: $color;
}
}
botton/index.ts
js
import {App} from 'vue'
import ElButton from './Button.vue'
export default {
install(app:App){
app.component(ElButton.name,ElButton)
}
}
botton/Button.vue
js
<template>
<button
class="el-button"
:class="[
buttonSize ? `el-button--${buttonSize}` : '',
props.type ? `el-button--${props.type}` : ''
]"
>
<slot />
</button>
</template>
<script setup lang="ts">
defineOptions({
name: 'ElButton'
})
import {computed, withDefaults} from 'vue'
import { useGlobalConfig } from '../../util';
interface Props {
size?:""|'small'|'medium'|'large',
type?:""|'primary'|'success'|'danger'
}
const props = withDefaults(defineProps<Props>(),{
size:"",
type:""
})
const globalConfig = useGlobalConfig()
const buttonSize = 'large' // 默认大尺寸
</script>
<style lang="scss">
@import '../styles/mixin';
@include b(button){
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
background: $--button-default-background-color;
color: $--button-default-font-color;
-webkit-appearance: none;
text-align: center;
border: $--border-base;
border-color: $--button-default-border-color;
box-sizing: border-box;
outline: none;
margin: 0;
font-weight: $--button-font-weight;
& + & {
margin-left: 10px;
}
@include button-size(
$--button-padding-vertical,
$--button-padding-horizontal,
$--button-font-size,
$--button-border-radius
);
&:hover,
&:focus {
color: $--color-primary;
border-color: mix($--color-white,$--color-primary,70%);
background-color: mix($--color-white,$--color-primary,90%);
}
@include m(medium) {
@include button-size(
$--button-medium-padding-vertical,
$--button-medium-padding-horizontal,
$--button-medium-font-size,
$--button-medium-border-radius
);
}
@include m(small) {
@include button-size(
$--button-small-padding-vertical,
$--button-small-padding-horizontal,
$--button-small-font-size,
$--button-small-border-radius
);
}
@include m(large) {
@include button-size(
$--button-large-padding-vertical,
$--button-large-padding-horizontal,
$--button-large-font-size,
$--button-large-border-radius
);
}
@include m(primary) {
@include button-variant(
$--button-primary-font-color,
$--button-primary-background-color,
$--button-primary-border-color
);
}
@include m(success) {
@include button-variant(
$--button-success-font-color,
$--button-success-background-color,
$--button-success-border-color
);
}
@include m(danger) {
@include button-variant(
$--button-danger-font-color,
$--button-danger-background-color,
$--button-danger-border-color
);
}
}
</style>
使用 App.vue
js
<el-button>
按钮
</el-button>
<el-button type="primary">
按钮
</el-button>
<el-button type="success">
按钮
</el-button>
<el-button>按钮</el-button>
<el-button size="small">
按钮
</el-button>
main.ts
js
import ElButton from './button'
const app = createApp(App)
app.use(ElButton).mount('#app')
效果图
