基于BEM规范实现ElementPlus组件

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')

效果图

相关推荐
@大迁世界1 天前
用 popover=“hint“ 打造友好的 HTML 提示:一招让界面更“懂人”
开发语言·前端·javascript·css·html
伍哥的传说1 天前
Tailwind CSS v4 终极指南:体验 Rust 驱动的闪电般性能与现代化 CSS 工作流
前端·css·rust·tailwindcss·tailwind css v4·lightning css·utility-first
拜无忧1 天前
前端,用SVG 模仿毛笔写字绘画,defs,filter
前端·css·svg
代码小学僧1 天前
🎉 在 Tailwind 中愉快的使用 Antd Design 色彩
前端·css·react.js
ssshooter1 天前
复习 CSS Flex 和 Grid 布局
前端·css·html
菲兹园长1 天前
CSS(展示效果)
前端·javascript·css
UNbuff_01 天前
HTML 中的 CSS 使用说明
css·html·tensorflow
很多石头1 天前
前端img与background-image渲染图片对H5页面性能的影响
前端·css
华仔啊1 天前
关于移动端100vh的坑和终极解决方案,看这一篇就够了!
前端·css