普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月25日首页

CSS渐变色边框的两种实现方案原理+对比与实战

作者 鹏多多
2025年11月25日 08:16

在现代 Web 开发中,渐变色边框是提升 UI 视觉层次感的常用设计元素。不同于纯色边框的简单实现,渐变色边框需要借助 CSS 背景、伪元素等特性完成。本文将详细解析两种主流实现方案的原理、代码细节,并补充兼容性说明与使用场景对比。

1. 方案一:伪元素 + mask 实现

使用伪元素 + mask 实现,灵活可控:

核心原理

通过父元素的伪元素(::before)创建全屏覆盖层,利用 padding 控制边框厚度,再通过 mask(遮罩)特性实现「中间透明、边缘显示渐变」的效果。核心逻辑如下:

  1. 伪元素 inset: 0 实现全屏覆盖父元素;
  2. padding: Npx 预留边框厚度(N 为边框宽度);
  3. 渐变背景作为伪元素底色;
  4. mask-composite 实现遮罩叠加,只保留边缘区域显示。

方案1

完整代码

<div class="gradient-border-1">按钮1内容</div>

<style>
.gradient-border-1 {
  position: relative; /* 关键:伪元素绝对定位的参考 */
  border-radius: 8px; /* 支持圆角 */
  overflow: hidden; /* 可选:防止内容溢出边框 */
  width: 200px;
  height: 100px;
  margin: 50px;
  /* 内容区域样式(与边框无关) */
  display: flex;
  align-items: center;
  justify-content: center;
  color: #333333;
}

/* 伪元素实现渐变边框 */
.gradient-border-1::before {
  content: ''; /* 伪元素必须有 content 才会显示 */
  position: absolute;
  inset: 0; /* 等价于 top/right/bottom/left: 0 */
  border-radius: inherit; /* 继承父元素圆角,避免边框直角 */
  padding: 3px; /* 边框厚度:3px */
  /* 渐变背景(可自定义方向和颜色) */
  background: linear-gradient(90deg, #9130fc 0%, #ff32e5 50%, #ffe485 100%);
  /* 遮罩:保留边缘边框,中间内容区域透明 */
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; /* 遮罩叠加模式:异或 */
  mask-composite: exclude; /* 标准语法:排除中间区域 */
  pointer-events: none; /* 关键:避免伪元素阻挡父元素的鼠标事件(如点击) */
  transition: ease 0.3s; /* 过渡动画 */
}

/* hover 动效:切换渐变颜色 */
.gradient-border-1:hover::before {
  background: linear-gradient(90deg, #ff3c00 0%, #ff7a00 50%, #9130fc 100%);
}
</style>

关键特性说明

  1. 边框厚度控制:通过 padding 直接调整(如 padding: 2px 对应 2px 边框);
  2. 圆角支持border-radius: inherit 确保边框与父元素圆角一致,无锯齿;
  3. 事件穿透pointer-events: none 保证父元素的点击、hover 等事件正常触发;
  4. 动效支持:直接修改伪元素的 background 渐变即可实现边框颜色过渡。

2. 方案二:双层背景 + background-clip 实现(简洁高效)

双层背景 + background-clip 实现(简洁高效):

核心原理

利用 CSS 多背景图层叠加特性,同时设置两个背景:

  1. 内层背景:纯色背景(与页面背景一致,模拟「内容区域透明」效果);
  2. 外层背景:渐变背景(作为边框颜色); 通过 background-clipbackground-origin 控制两个背景的显示范围:
  • padding-box:内层背景仅显示在 padding 区域(内容区域);
  • border-box:外层背景显示在边框区域;
  • 父元素设置 border: Npx solid transparent,让边框区域显示外层渐变背景。

方案2

完整代码

<div class="gradient-border-2">按钮2内容</div>

<style>
.gradient-border-2 {
  width: 200px;
  height: 100px;
  margin: 50px;
  border-radius: 8px; /* 支持圆角 */
  /* 关键:透明边框 + 双层背景 */
  border: 3px solid transparent;
  background: 
    linear-gradient(#f1f1f1, #f1f1f1) padding-box, /* 内层:内容区域背景 */
    linear-gradient(90deg, #9130fc 0%, #ff32e5 50%, #ffe485 100%) border-box; /* 外层:边框渐变 */
  background-origin: padding-box, border-box; /* 背景起始位置:分别对应 padding 和 border */
  background-clip: padding-box, border-box; /* 背景裁剪:仅显示在对应区域 */
  /* 内容区域样式 */
  display: flex;
  align-items: center;
  justify-content: center;
  color: #333333;
  transition: ease 0.3s; /* 过渡动画 */
}

/* hover 动效:切换外层渐变背景 */
.gradient-border-2:hover {
  background: 
    linear-gradient(#f1f1f1, #f1f1f1) padding-box, /* 内层背景不变 */
    linear-gradient(90deg, #ff3c00 0%, #ff7a00 50%, #9130fc 100%) border-box; /* 外层渐变切换 */
}
</style>

关键特性说明

  1. 简洁性:无需伪元素,仅通过父元素的背景和边框属性实现;
  2. 边框厚度控制:直接修改 border 的宽度(如 border: 4px solid transparent);
  3. 背景联动:内层背景需与页面背景一致(示例中为 #f1f1f1),否则会露出内层背景色;
  4. 无事件冲突:无需额外处理鼠标事件,天然支持父元素交互。

3. 兼容性对比

方案一(伪元素 + mask)

特性 支持浏览器版本 备注
mask / -webkit-mask Chrome 4+、Firefox 53+、Safari 6+、Edge 79+ 需添加 -webkit- 前缀
mask-composite Chrome 4+、Firefox 53+、Safari 6+、Edge 79+ 标准语法 exclude 需配合前缀
圆角支持 所有现代浏览器 无兼容性问题
过渡动画 所有现代浏览器 支持渐变背景过渡

兼容性结论:支持所有现代浏览器,IE 完全不支持(mask 特性未实现)。适合面向移动端、PC 端现代浏览器的场景。

方案二(双层背景 + background-clip)

特性 支持浏览器版本 备注
background-clip Chrome 1+、Firefox 4+、Safari 3.1+、Edge 12+、IE 9+ 部分旧浏览器需前缀 -webkit-
background-origin Chrome 1+、Firefox 4+、Safari 3.1+、Edge 12+、IE 9+ 无明显兼容性问题
多背景叠加 Chrome 1+、Firefox 3.6+、Safari 3.1+、Edge 12+、IE 9+ 支持多层背景叠加
圆角支持 所有现代浏览器 无兼容性问题

兼容性结论:兼容性更优,支持 IE 9+ 及所有现代浏览器。适合需要兼容旧版浏览器(如 IE 11)的场景。

4. 两种方案对比与使用场景

对比维度 方案一(伪元素 + mask) 方案二(双层背景)
实现复杂度 中等(需理解 mask 遮罩原理) 简单(仅需掌握背景属性)
兼容性 现代浏览器友好(IE 不支持) 更广泛(支持 IE 9+)
内容背景依赖 无(伪元素独立,内容区域背景可自由设置) 有(内层背景需与页面背景一致)
边框效果灵活性 高(支持复杂遮罩,如渐变+透明度) 中等(仅支持渐变背景)
性能开销 略高(伪元素 + mask 计算) 更低(纯背景渲染,无额外元素)

推荐使用场景

  1. 方案一

    • 需兼容现代浏览器(Chrome/Firefox/Safari/Edge);
    • 内容区域背景复杂(如图片背景、动态背景);
    • 需实现更灵活的边框效果(如渐变+半透明、多色叠加)。
  2. 方案二

    • 需兼容旧版浏览器(如 IE 11);
    • 内容区域的背景色需要为纯色,且与页面背景一致;
    • 追求简洁代码,无需额外伪元素。

5. 进阶优化建议

  1. 渐变方向自定义:将 linear-gradient 改为 radial-gradient(径向渐变)或 conic-gradient(锥形渐变),实现更多样的边框效果;
  2. 响应式边框:使用相对单位(如 emvw)替代固定像素,适配不同屏幕尺寸;
  3. 性能优化:方案一中可添加 will-change: background 提升过渡动画性能;
  4. 兼容性前缀:使用 Autoprefixer 自动添加浏览器前缀(如 -webkit-mask-webkit-background-clip),减少手动编写成本。

6. 总结

两种方案均能实现高质量的渐变色边框,核心差异在于兼容性和使用场景。方案一通过伪元素+mask 提供更高的灵活性,适合现代浏览器环境;方案二通过双层背景实现更简洁的代码和更广泛的兼容性,适合需要兼容旧版浏览器的场景。实际开发中可根据项目的浏览器支持范围、内容背景复杂度选择合适的方案,同时结合进阶优化技巧提升视觉效果和性能。


本次分享就到这儿啦,我是鹏多多,深耕前端的技术创作者,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

PS:在本页按F12,在console中输入document.getElementsByClassName('panel-btn')[0].click();有惊喜哦~

往期文章

昨天 — 2025年11月24日首页
昨天以前首页

从 0 到 1 实现 LocalStorage 待办清单:CSS 进阶 + 前端工程化思想实践

2025年11月23日 10:21

在前端开发中,LocalStorage 是浏览器提供的核心本地存储方案,而 CSS 布局与工程化编码则是提升开发效率和代码质量的关键。本文将结合实际案例,带大家从零实现一个可持久化的待办清单,同时拆解 CSS 继承 / 弹性布局的核心用法,以及前端函数式封装的工程化思想。

一、核心知识点预热:先搞懂这 3 个关键技术

在动手写代码前,我们先梳理案例中涉及的核心技术点,帮大家打通知识盲区:

1. LocalStorage:浏览器的「永久储物柜」

  • 存储位置:浏览器本地,与服务器无关

  • 存储特性:永久存储(除非手动清除或代码删除),页面刷新 / 关闭后数据不丢失

  • 存储规则:仅支持 key-value 键值对,且值必须是字符串(对象 / 数组需用 JSON.stringify() 序列化)

  • 核心 API

    javascript

    运行

    // 存数据
    localStorage.setItem('key', JSON.stringify(数据));
    // 取数据(需反序列化)
    JSON.parse(localStorage.getItem('key'));
    // 删数据(单个/全部)
    localStorage.removeItem('key');
    localStorage.clear();
    

2. CSS 关键特性:这些细节能少写 80% 冗余代码

(1)继承性:不是所有属性都能「子承父业」

  • 可继承属性font-sizecolortext-align 等文本相关属性(子元素自动继承父元素样式)

  • 不可继承属性backgroundwidthheightborder 等布局 / 盒模型相关属性(需手动设置)

  • 实用技巧:用 inherit 强制继承父属性,比如让子元素高度跟随父元素:

    css

    .child {
      height: inherit; /* 继承父元素 height */
    }
    

(2)outline 与 overflow:细节优化神器

  • outline:元素轮廓,类似 border 但不占据盒模型空间(适合表单聚焦样式)
  • overflow:控制子元素超出父元素时的表现(hidden 隐藏溢出、auto 自动滚动)

(3)Flex 布局:快速实现居中与对齐

Flex 是现代布局方案,核心是「格式化上下文」,只需 3 行代码实现元素居中:

css

.parent {
  display: flex; /* 开启 Flex 布局 */
  justify-content: center; /* 主轴(水平)居中 */
  align-items: center; /* 交叉轴(垂直)居中 */
}

3. 前端工程化:拒绝流程式代码,拥抱函数封装

当代码超过 10 行,就该考虑封装函数!核心优势:

  • 复用性:同一逻辑多处调用,减少冗余
  • 可维护性:隐藏实现细节,修改时只需改函数内部
  • 可读性:函数名即功能,代码逻辑更清晰

二、实战:实现 LocalStorage 待办清单

1. 项目结构

plaintext

todo-list/
├── common.css  # 样式文件
└── index.html  # 结构+逻辑文件

2. 样式文件:common.css

css

/* 基础重置:统一盒模型 */
html {
  box-sizing: border-box;
  min-height: 100vh; /* 占满视口高度 */
  display: flex; /* 页面整体居中 */
  justify-content: center;
  align-items: center;
  text-align: center; /* 文本居中 */
  background-color: #f5f5f5;
}

*,
*::before,
*::after {
  box-sizing: inherit; /* 继承盒模型 */
}

/* 容器样式 */
.wrapper {
  padding: 20px;
  max-width: 350px;
  background-color: rgba(255, 255, 255, 0.95);
  box-shadow: 0 0 0 10px rgba(0, 0, 0, 0.1); /* 外发光效果 */
  border-radius: 8px;
}

h2 {
  margin: 0 0 20px;
  font-weight: 200;
  color: #333;
}

/* 待办列表样式 */
.plates {
  margin: 0;
  padding: 0;
  text-align: left;
  list-style: none;
}

.plates li {
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  padding: 10px 0;
  font-weight: 100;
  display: flex; /* 复选框与文本同行 */
  align-items: center;
}

.plates label {
  flex: 1; /* 文本占满剩余空间 */
  cursor: pointer; /* 鼠标悬浮变指针 */
  color: #444;
}

/* 自定义复选框样式 */
.plates input {
  display: none; /* 隐藏原生复选框 */
}

.plates input + label:before {
  content: "⬜️";
  margin-right: 10px;
  font-size: 18px;
}

.plates input:checked + label:before {
  content: "✅"; /* 选中时切换图标 */
}

/* 表单样式 */
.add-items {
  margin-top: 20px;
  display: flex;
  gap: 10px; /* 输入框与按钮间距 */
}

.add-items input[type="text"] {
  flex: 1;
  padding: 10px;
  border: 1px solid rgba(0, 0, 0, 0.1);
  border-radius: 4px;
  outline: 3px solid transparent;
  transition: outline 0.3s;
}

.add-items input[type="text"]:focus {
  outline: 3px solid rgba(14, 14, 211, 0.8); /* 聚焦高亮 */
}

.add-items input[type="submit"] {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background-color: rgba(14, 14, 211, 0.8);
  color: white;
  cursor: pointer;
  transition: background-color 0.3s;
}

.add-items input[type="submit"]:hover {
  background-color: rgba(14, 14, 211, 1);
}

3. 核心文件:index.html

html

预览

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>LocalStorage 待办清单</title>
  <link rel="stylesheet" href="./common.css">
</head>
<body>
  <div class="wrapper">
    <h2>LOCAL TAPAS</h2>
    <ul class="plates">
      <li>Loading Tapas...</li>
    </ul>
    <form class="add-items">
      <input 
        type="text" 
        placeholder="输入待办事项" 
        required
        name="item"
      >
      <input type="submit" value="+ 添加事项">
    </form>
  </div>

  <script>
    // 1. 获取 DOM 元素
    const addItemsForm = document.querySelector('.add-items');
    const itemsList = document.querySelector('.plates');
    // 2. 从 LocalStorage 取数据(无则初始化空数组)
    const items = JSON.parse(localStorage.getItem('todos')) || [];

    /**
     * 渲染待办列表
     * @param {Array} plates - 待办数组
     * @param {Element} platesList - 列表 DOM 元素
     */
    function populateList(plates = [], platesList) {
      // 数组 map 生成 DOM 字符串,join 拼接(避免逗号分隔)
      platesList.innerHTML = plates.map((plate, index) => `
        <li>
          <input 
            type="checkbox" 
            data-index="${index}" 
            id="item${index}"
            ${plate.done ? 'checked' : ''}
          />
          <label for="item${index}">${plate.text}</label>
        </li>
      `).join('');
    }

    /**
     * 添加待办事项
     * @param {Event} event - 表单提交事件
     */
    function addItem(event) {
      event.preventDefault(); // 阻止表单默认提交行为
      // 获取输入框值(去空格)
      const text = this.querySelector('[name=item]').value.trim();
      if (!text) return; // 空值不添加

      // 新增待办对象
      const newItem = {
        text,
        done: false // 默认未完成
      };

      items.push(newItem);
      // 存入 LocalStorage(序列化数组)
      localStorage.setItem('todos', JSON.stringify(items));
      // 重新渲染列表
      populateList(items, itemsList);
      this.reset(); // 重置表单
    }

    /**
     * 切换待办完成状态
     * @param {Event} event - 点击事件
     */
    function toggleDone(event) {
      const target = event.target;
      // 只处理复选框的点击
      if (target.tagName !== 'INPUT') return;

      // 获取数据索引(从 data-index 属性)
      const index = target.dataset.index;
      // 切换完成状态
      items[index].done = !items[index].done;
      // 更新 LocalStorage
      localStorage.setItem('todos', JSON.stringify(items));
      // 重新渲染
      populateList(items, itemsList);
    }

    // 3. 绑定事件监听
    addItemsForm.addEventListener('submit', addItem);
    itemsList.addEventListener('click', toggleDone);

    // 4. 页面加载时渲染列表
    populateList(items, itemsList);
  </script>
</body>
</html>

三、关键技术拆解:为什么这么写?

1. LocalStorage 持久化逻辑

  • 初始化:页面加载时从 localStorage 读取 todos 数据,若不存在则初始化为空数组
  • 新增待办:添加后立即用 JSON.stringify() 序列化数组,存入 localStorage
  • 状态切换:修改待办完成状态后,同步更新 localStorage,确保刷新后状态不丢失

2. CSS 进阶技巧

  • 盒模型继承:通过 box-sizing: inherit 让所有元素(包括伪元素)继承 border-box,避免计算宽度时的麻烦
  • Flex 布局妙用:页面整体居中、待办项复选框与文本对齐、表单输入框与按钮同行分布,都用 Flex 实现,简洁高效
  • 自定义复选框:隐藏原生复选框,用 :before 伪元素实现自定义图标,提升视觉体验
  • 聚焦状态优化:输入框聚焦时添加蓝色轮廓,提升交互反馈

3. 工程化编码思想

(1)函数式封装:拒绝流程式代码

  • 把「渲染列表」「添加待办」「切换状态」拆分为 3 个独立函数,每个函数只做一件事
  • 函数参数化:populateList 接收待办数组和列表 DOM 元素,增强复用性
  • 避免全局变量污染:核心数据 items 通过函数参数传递,而非依赖全局变量

(2)细节优化:提升用户体验

  • 输入框去空格:用 trim() 避免添加空待办
  • 空值判断:输入为空时不添加待办
  • 表单重置:添加后清空输入框,方便连续输入
  • 事件委托:给列表父元素绑定点击事件,而非给每个复选框绑定,提升性能(尤其待办较多时)

四、扩展与优化方向

  1. 添加删除功能:给每个待办项增加删除按钮,点击时删除对应项并更新 localStorage
  2. 批量操作:实现「全选 / 全不选」「清空已完成」功能
  3. 数据持久化优化:封装 localStorage 操作工具函数,避免重复写 JSON.stringify 和 JSON.parse
  4. 样式升级:添加动画效果(如待办项添加 / 删除时的过渡动画)
  5. 响应式优化:适配移动端,优化小屏幕下的布局

五、总结

本文通过一个简单的待办清单案例,串联了 LocalStorage 本地存储、CSS 进阶特性和前端工程化编码思想。核心要点:

  • LocalStorage 是前端持久化存储的基础,关键是掌握「序列化 / 反序列化」技巧
  • CSS 不仅是样式,合理运用继承、Flex 布局和伪元素,能大幅提升开发效率和视觉体验
  • 工程化编码的核心是「拆分与封装」,让代码更易维护、可复用

这个案例虽然简单,但包含了前端开发的核心思想,适合新手入门练习,也能帮助有经验的开发者巩固基础。动手试试扩展功能,让它变得更强大吧!

为什么有些人边框不用border属性

作者 爆浆麻花
2025年11月22日 17:46

1) border 会改变布局(占据空间)

border 会参与盒模型,增加元素尺寸。

例如,一个宽度 200px 的元素加上 border: 1px solid #000,实际宽度会变成:

200 + 1px(left) + 1px(right) = 202px

如果不想影响布局,就很麻烦。

使用 box-shadow: 0 0 0 1px #000不会改变大小,看起来像 border,但不占空间。


2) border 在高 DPI 设备上容易出现“模糊/不齐”

特别是 0.5px border(发丝线),在某些浏览器上有锯齿、断线。

transform: scale(0.5) 或伪元素能做更稳定的发丝线。


3) border 圆角 + 发丝线 常出现不规则效果

border + border-radius 在不同浏览器的渲染不一致,容易出现不均匀、颜色不一致的问题。

outline / box-shadow 圆角更稳定。


4) border 不适合做阴影/多层边框

如果你需要两层边框:

双层边框用 border 很难做

而用:

box-shadow: 0 0 0 1px #333, 0 0 0 2px #999;

非常简单。


5) border 和背景裁剪一起用时容易出 bug

比如 background-clipoverflow: hidden 配合 border 会出现背景被挤压、不应该被裁剪却裁剪等问题。


6) hover/active 等状态切换时会“跳动”

因为 border 会改变元素大小。

例子:

.btn { border: 0; }
.btn:hover { border: 1px solid #000; }

鼠标移上去会抖动,因为尺寸变大了。

box-shadow 的话就不会跳。

总结

边框可以分别使用border、outline、box-shadow三种方式去实现,其中outline、box-shadow不会像border一样占据空间。而box-shadow可以用来解决两个元素相邻时边框变宽的问题。所以outline和box-shadow的兼容性和灵活性会更好一点

用 localStorage 打造本地待办清单:一个轻量级的前端实践

作者 www_stdio
2025年11月22日 16:41

用 localStorage 打造本地待办清单:一个轻量级的前端实践

在现代网页开发中,我们常常需要在浏览器端保存一些用户数据,比如用户的偏好设置、临时输入内容,或者像本文要实现的——一个本地待办事项列表(Todo List)。借助浏览器提供的 localStorage,我们可以轻松地将数据持久化存储在用户的设备上,即使关闭页面或重启浏览器,数据也不会丢失。

localStorage 是什么?

localStorage 是 Web Storage API 的一部分,它为每个域名提供了一块独立的存储空间。它的特点是:

  • 永久存储:除非用户手动清除或通过代码删除,否则数据不会过期。
  • 键值对结构:所有数据都以字符串形式存储,因此通常需要配合 JSON.stringify()JSON.parse() 来处理对象。
  • 同源策略限制:只能被同一协议、域名和端口下的页面访问。

实现一个本地待办清单

下面是一个完整的待办清单示例,使用 HTML、CSS 和原生 JavaScript 构建,并利用 localStorage 实现数据持久化。

HTML 结构

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LOCAL TAPAS</title>
    <link rel="stylesheet" href="./common.css">
</head>
<body>
    <div class="wrapper">
        <h2>LOCAL TAPAS</h2>
        <ul class="plates">
            <li>Loading Tapas...</li>
        </ul>
        <form class="add-items">
            <input type="text" placeholder="Item Name" required name="item">
            <input type="submit" value="+ Add Item">
        </form>
    </div>
    <!-- JavaScript 脚本 -->
</body>
</html>

页面包含一个标题、一个待办项列表容器(<ul class="plates">)以及一个用于添加新事项的表单。

CSS 样式亮点

html {
    box-sizing: border-box;
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
}

*, *::before, *::after {
    box-sizing: inherit;
}

.plates li {
    display: flex;
}

.plates input {
    display: none;
}

.plates input + label:before {
    content: "⬜️";
    margin-right: 10px;
}

.plates input:checked + label:before {
    content: "✅";
}

这里有几个关键点:

  • 使用 flex 布局让页面整体居中,同时让每个待办项内部也采用弹性布局。
  • 利用 input[type=checkbox] 配合 label 和伪元素 ::before 实现自定义复选框样式。
  • outline 属性用于高亮输入框焦点状态,且不占用盒模型空间。
  • overflow: hidden 可防止子元素溢出父容器(虽然本例未直接使用,但属于常用技巧)。

注意:CSS 中并非所有属性都会继承。例如 font-sizecolor 会从父元素继承,而 backgroundwidthheight 等则不会。开发者需根据需求显式设置。

JavaScript 逻辑

核心逻辑围绕三个函数展开:

1. 初始化数据
const items = JSON.parse(localStorage.getItem('todos')) || [];

尝试从 localStorage 中读取名为 'todos' 的数据,若不存在则初始化为空数组。

2. 渲染列表
function populateList(plates = [], platesList) {
    platesList.innerHTML = plates.map((plate, i) => `
        <li>
            <input type="checkbox" data-index=${i} id="item${i}" ${plate.done ? 'checked' : ''} />
            <label for="item${i}">${plate.text}</label>
        </li>
    `).join('');
}

该函数接收待办项数组和 DOM 容器,动态生成带复选框的列表项,并通过 data-index 记录索引以便后续操作。

3. 添加与切换状态
function addItem(event) {
    event.preventDefault();
    const text = this.querySelector('[name=item]').value.trim();
    if (!text) return;
    items.push({ text, done: false });
    localStorage.setItem('todos', JSON.stringify(items));
    populateList(items, itemsList);
    this.reset();
}

function toggleDone(event) {
    if (event.target.tagName === 'INPUT') {
        const index = event.target.dataset.index;
        items[index].done = !items[index].done;
        localStorage.setItem('todos', JSON.stringify(items));
        populateList(items, itemsList);
    }
}
  • 表单提交时阻止默认刷新行为,提取输入值并存入 items 数组。
  • 点击复选框时,根据 data-index 更新对应项的完成状态。
  • 每次变更后立即同步到 localStorage,确保数据持久化。

最后绑定事件监听器:

addItems.addEventListener('submit', addItem);
itemsList.addEventListener('click', toggleDone);
populateList(items, itemsList); // 初次渲染

小结

这个“Local Tapas”(本地小食清单)虽小,却完整展示了前端开发中的多个核心概念:

  • 数据持久化:通过 localStorage 实现无服务器依赖的本地存储。
  • DOM 操作与事件委托:高效更新界面,避免重复绑定事件。
  • CSS 继承与布局:理解哪些样式可继承,合理使用 Flexbox 实现响应式结构。
  • 函数式思维:将逻辑封装为可复用函数,提升代码可读性与维护性。

无需后端、不依赖框架,仅用浏览器原生能力,就能构建一个实用又美观的交互应用——这正是现代 Web 开发的魅力所在。

面试问“如何设计可扩展 Button”?像素风格button改造只要 10分钟!

作者 孟祥_成都
2025年11月20日 14:59

前言

面试官:(打着哈欠,眼皮打架)你说你写的组件库拓展性很好,怎么证明呢?

:(自信满满)您直接去我的组件库官网看,这是传统组件库样式,大家都能做:

image.png

地址:上图页面地址

:(切换页面)然后这是我用自己写的 @t-headless-ui/react 中的同一个 Button 组件,只是稍微改了下样式。比如你想要像素风格,请看

image.png

地址:上图页面地址

(如果您觉得不错,请求 github 一个赞,我一定会拓展所有主流组件到其中的!)

面试官:(原本昏昏欲睡,突然瞪大眼睛,从凳子上一跃而起)"卧槽!兄弟!你这...这怎么做到的?!"

:(淡定一笑)无它,headless 组件库而已!

面试官:(扶了扶惊掉的下巴)"headless 组件库是什么意思?"

:"简单说就是——我们只提供JavaScript逻辑和功能,样式你爱咋写咋写!甚至连DOM结构都随便你折腾!"

面试官:(沉默三秒,突然起身握住我的手)"好!我服!明天就来上班!工资你随便开!"

当然,上面只是一个小玩笑,哈哈,今天我们就来介绍一下,如何写一个像素风格的 button 以及为啥我的 headless button 稍微改一下样式,就可以造一个生产级别能用的 button 呢?

一个生产环境要用的 button 需要具备什么样的属性呢?

button 的状态

一个 button 至少需要:

  • 定义 disabled 状态,在 disabled 状态, click 事件是无法生效的
  • 定义 loading 状态,在 loading 状态, click 事件同样是是无法生效的

headless 也就是无样式组件的层面上,做到拦截这两个属性的 click 事件就足够了.

是不是很简单,只是因为 button 的复杂度更多在于定制样式,所以 headless 组件库的 button 很容易实现。

核心代码如下:

  const handleClick: MouseEventHandler = (event): void => {
    if (loading || disabled) {
      // 阻止按钮默认行为,防止在 loading 或 disabled 状态下意外提交表单或触发其他默认事件
      event?.preventDefault?.();
      return;
    }
    onClick?.(event);
  };

好了,接下来我们说样式,样式的难度主要在于状态很多,包括:

  • 各种主题色
    • primary:主色
    • success: 成功色
    • error:失败颜色
    • 。。。 然后每个状态都分为,default 状态,disabled 状态,loading 状态
  • 各种风格
    • 有背景色的风格
    • 只有边框的风格
    • 。。。

然后每个状态都分为,default 状态,disabled 状态,loading 状态

还有需要考虑 focus 上的颜色状态,hover 的颜色状态。

这才是一个 button 真正复杂的点!

如果你想学造组件库,欢迎加入,我们超级可拓展的 headless 组件库交流群(还有包装简历服务哦!)主页

我们现在为了说明这个像素 button 实现的思路,我们化繁为简,假设只有 1 种颜色,1 种风格,如下图:

image.png

说明实现思路是如何,大家可以触类旁通!

如何制作出像素效果

从上面图可以看出,最明显的像素想过就是我们死角都是 transparent(透明色),这个明显是 border 实现不了的。那如何做呢?

秘密就是 box-shadow !

我们先举一个小例子,你看看 box-shadow 如何使用,假设我们需要一个下边框,box-shadow 设置为:

box-shadow: 0 5px black;

其中 0 代表横向的偏移量,我们是 0 ,也就是横轴上什么也不做,5px 代表在竖直方向上向下制造一个 5pxshadow, 然后颜色设置为 black 也就是黑色,请看这个按钮下方多了一条黑色

image.png

接着,我们依葫芦画瓢,加上 上面的 shadow

box-shadow: 0 5px black, 0 -5px black;

效果如下

image.png

接着右边我们也加一下:

box-shadow: 0 5px black, 0 -5px black, 5px 0 black;

image.png

是不是有感觉了,shadowborder 最大的不同就是边角不会填充颜色,然后接着,我们加上又边框:

box-shadow: 0 5px black, 0 -5px black, 5px 0 black, -5px 0 black;

最后我们再润色一下,加一个上阴影,增加立体感,如下图

image.png

box-shadow: 0 5px black, 0 -5px black, 5px 0 black, -5px 0 black, inset 0px 4px rgba(255,255,255,0.21);

其中 inset 的意思是增加内阴影,之前我们都是外阴影,rgba(255,255,255,0.21) 是指白色,然后 0.21 的透明度。

初始化 button

在我们设置 box-shadow 之前,这个 button 背景色,字体这些还没初始化,我们先来补上这个知识

{
    position: relative;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    user-select: none;
    padding-top: .25rem;
    padding-bottom: .25rem;   
    padding-left: 1.25rem;
    padding-right: 1.25rem;
    background-color: rgb(88,199,192);
    border-width: 0;
    border-radius: 0;
}

好了初始化完毕,我们开始处理各种状态:

hover 状态

hover 状态我们用了一个 filter 函数,增加亮度。

filter:brightness(1.05)

但是同时注意,在 disabledloading 状态 hover 无效。在 react 一般都要使用 classnames 库,tailwindcss 还要将 classnamestailwindcss-merge 库结合, vue 好像天然对于 css 的条件判断就支持的不错。

类似代码如下(不同的框架和使用的 css 框架不同,代码肯定有不同,这个很简单,大家看得懂意思即可):

{
   'hover:brightness-105': !disabled && !loading
}

'hover:brightness-105' 是 tailwindcss 中的类名, 对于普通的 css 你只要换成你希望的类名,然后引入对应的 css 文件即可(在 css 文件里实现这个 hover 的 css).

active 状态

active 状态,我们只是把内阴影变黑了,给人一种凹凸下去的感觉。 active 下的 box-shadow 变为

box-shadow: 0px 5px black,0px -5px black,5px 0px black,-5px 0px black,inset 0px 5px #00000038

可以看到只是最后 inset 中的颜色变深了,其它都没有变。

处理 disabled 状态

这个状态其实需要判断,比如 disabled 的时候 cursor ,也就是鼠标显示一个禁止点击的状态,这样有利于告诉用户此时点击无效。

类似的增加对 disabled 状态的处理:

{
    'opacity-50 cursor-not-allowed': disabled, // disabled 时透明度降低,cursor 变为 not-allowed
}

字体更想像素的样子

这个最好是搜索有像素风格的字体样式,我是为了方便,使用系统自带中,类似有像素风格的字体,大家可以参考,兼容macwindows

{
    font-family: 'ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Lucida_Console','Courier_New',monospace'
}

dark 模式

我使用的是 tailwindcss 天然对 dark 模式可以自定义颜色,国内很多人不使用 taiwindcss 这里就不赘述了

小总结

这里就结束了,这一种状态看起来代码不多,其实状态和颜色多了之后,css 代码会非常多。最后,大家可以看到 headless 组件的好处就是跟样式没有任何耦合,随便用各种 css 框架自己处理,它的好处我会在后面的文章继续介绍,例如 radio 组件,本质上就是单选,我们看到很多组件库都是一个圆形按钮的样式,其实谁规定了必须是这个样式?我可以是任何只要满足单选的样式即可。

所以后续会有很多有趣的主题加入进来,包括button,也会持续更新更重好玩的样式和动画!

最后你到看到这里了,欢迎各位大侠来组件库交流群交流,也希望各位大侠给个github的赞,嘿嘿

20个CSS实用技巧,10分钟从小白变大神!

作者 刘大华
2025年11月21日 10:48

大家好,我是大华!今天给大家分享一些比较实用的CSS技巧,不管你是前端新手还是老司机,这些技巧都能让你的开发效率翻倍!废话不多说,直接上代码了。

1. 使用变量维护主题色

/* 定义CSS变量 */
:root {
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --text-color: #333;
}

/* 使用变量 */
.button {
  background-color: var(--primary-color);
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
}

.card {
  border: 1px solid var(--secondary-color);
  color: var(--text-color);
}

适用场景:需要维护统一设计规范的项目,方便快速切换主题

2. Flex布局居中(终极解决方案)

.center-container {
  display: flex;
  justify-content: center; /* 水平居中 */
  align-items: center;     /* 垂直居中 */
  height: 100vh;          /* 视口高度 */
}

/* 如果只需要单方向居中 */
.vertical-center {
  display: flex;
  align-items: center;    /* 仅垂直居中 */
}

.horizontal-center {
  display: flex;
  justify-content: center; /* 仅水平居中 */
}

适用场景:任何需要居中的布局,特别是响应式设计

3. Grid网格布局

.grid-container {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 3等分列 */
  grid-gap: 20px;                       /* 网格间距 */
  padding: 20px;
}

.grid-item {
  background: #f0f0f0;
  padding: 20px;
  border-radius: 8px;
}

/* 响应式网格 */
@media (max-width: 768px) {
  .grid-container {
    grid-template-columns: repeat(2, 1fr); /* 小屏幕2列 */
  }
}

@media (max-width: 480px) {
  .grid-container {
    grid-template-columns: 1fr; /* 超小屏幕1列 */
  }
}

适用场景:复杂布局、卡片布局、图片画廊

4. 自定义滚动条

.custom-scrollbar {
  width: 300px;
  height: 200px;
  overflow-y: scroll;
  padding: 10px;
}

/* Webkit浏览器滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
  width: 8px; /* 滚动条宽度 */
}

.custom-scrollbar::-webkit-scrollbar-track {
  background: #f1f1f1; /* 轨道背景 */
  border-radius: 4px;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
  background: #c1c1c1; /* 滑块颜色 */
  border-radius: 4px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8; /* 悬停颜色 */
}

适用场景:需要统一浏览器滚动条样式的项目

5. 文字渐变效果

.gradient-text {
  background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
  -webkit-background-clip: text; /* 关键:背景裁剪到文字 */
  -webkit-text-fill-color: transparent; /* 关键:文字透明 */
  background-clip: text;
  font-size: 3rem;
  font-weight: bold;
}

适用场景:标题设计、品牌文字、特色展示

6. 毛玻璃效果

.frosted-glass {
  background: rgba(255, 255, 255, 0.25); /* 半透明背景 */
  backdrop-filter: blur(10px);           /* 背景模糊 */
  -webkit-backdrop-filter: blur(10px);   /* Safari支持 */
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 10px;
  padding: 20px;
  color: white;
}

适用场景:现代设计风格、卡片悬浮效果、导航栏

7. 悬浮阴影动画

.card {
  background: white;
  border-radius: 10px;
  padding: 20px;
  transition: all 0.3s ease; /* 所有属性过渡0.3秒 */
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.card:hover {
  transform: translateY(-5px); /* 悬浮上移5px */
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); /* 阴影加深 */
}

适用场景:卡片设计、按钮交互、产品展示

8. 自定义复选框

/* 隐藏原生复选框 */
.custom-checkbox input[type="checkbox"] {
  display: none;
}

/* 自定义复选框样式 */
.checkbox-label {
  display: flex;
  align-items: center;
  cursor: pointer;
  font-size: 16px;
}

.checkmark {
  width: 20px;
  height: 20px;
  border: 2px solid #ddd;
  border-radius: 4px;
  margin-right: 10px;
  position: relative;
  transition: all 0.3s ease;
}

/* 选中状态 */
.custom-checkbox input[type="checkbox"]:checked + .checkbox-label .checkmark {
  background: #3498db;
  border-color: #3498db;
}

/* 选中后的对勾 */
.custom-checkbox input[type="checkbox"]:checked + .checkbox-label .checkmark::after {
  content: "";
  position: absolute;
  left: 6px;
  top: 2px;
  width: 6px;
  height: 10px;
  border: solid white;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

适用场景:表单美化、统一设计语言

9. 图片悬浮放大

.image-container {
  width: 300px;
  height: 200px;
  overflow: hidden; /* 隐藏溢出部分 */
  border-radius: 8px;
}

.zoom-image {
  width: 100%;
  height: 100%;
  object-fit: cover; /* 保持图片比例 */
  transition: transform 0.5s ease; /* 缩放过渡 */
}

.zoom-image:hover {
  transform: scale(1.1); /* 悬浮放大1.1倍 */
}

适用场景:产品图集、相册、作品展示

10. 文字阴影效果

.text-shadow {
  font-size: 3rem;
  font-weight: bold;
  color: #2c3e50;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); /* 水平 垂直 模糊 颜色 */
}

/* 多层阴影效果 */
.multi-shadow {
  font-size: 3rem;
  font-weight: bold;
  color: #e74c3c;
  text-shadow: 
    1px 1px 0 #c0392b,
    2px 2px 0 #922b21,
    3px 3px 5px rgba(0, 0, 0, 0.6);
}

适用场景:标题设计、文字特效、海报设计

11. 渐变边框

.gradient-border {
  padding: 20px;
  border: 4px solid;
  border-image: linear-gradient(45deg, #ff6b6b, #4ecdc4) 1;
  border-radius: 8px;
  background: white;
}

/* 另一种实现方式 */
.gradient-border-alt {
  padding: 20px;
  background: 
    linear-gradient(white, white) padding-box,
    linear-gradient(45deg, #ff6b6b, #4ecdc4) border-box;
  border: 4px solid transparent;
  border-radius: 8px;
}

适用场景:特色边框、高亮元素、品牌标识

12. 粘性定位

.sticky-header {
  position: sticky;
  top: 0; /* 距离顶部0时固定 */
  background: white;
  padding: 15px 0;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  z-index: 1000; /* 确保在最上层 */
}

.sticky-sidebar {
  position: sticky;
  top: 100px; /* 距离顶部100px时固定 */
  align-self: start; /* Grid布局中对齐 */
}

适用场景:导航栏、侧边栏、表头固定

13. 文字溢出省略

/* 单行文字溢出显示省略号 */
.single-line-ellipsis {
  white-space: nowrap;      /* 不换行 */
  overflow: hidden;         /* 隐藏溢出 */
  text-overflow: ellipsis;  /* 显示省略号 */
  width: 200px;
}

/* 多行文字溢出显示省略号 */
.multi-line-ellipsis {
  display: -webkit-box;
  -webkit-line-clamp: 3;    /* 显示3行 */
  -webkit-box-orient: vertical;
  overflow: hidden;
  width: 300px;
}

适用场景:标题截断、内容摘要、表格单元格

14. 自定义选择文本样式

::selection {
  background: #3498db;    /* 选中背景色 */
  color: white;           /* 选中文字颜色 */
  text-shadow: none;      /* 去除文字阴影 */
}

/* 针对不同浏览器前缀 */
::-moz-selection {
  background: #3498db;
  color: white;
  text-shadow: none;
}

适用场景:提升用户体验、品牌一致性

15. 滤镜效果

.image-filters {
  transition: filter 0.3s ease;
}

/* 灰度效果 */
.grayscale {
  filter: grayscale(100%);
}

.grayscale:hover {
  filter: grayscale(0%);
}

/* 多个滤镜组合 */
.multiple-filters {
  filter: brightness(1.2) contrast(0.8) saturate(1.5);
}

/* 模糊效果 */
.blur-effect {
  filter: blur(2px);
}

适用场景:图片特效、主题切换、视觉设计

16. 动画关键帧

@keyframes bounce {
  0%, 100% {
    transform: translateY(0); /* 起始和结束位置 */
  }
  50% {
    transform: translateY(-20px); /* 跳起位置 */
  }
}

.bouncing-element {
  animation: bounce 2s ease-in-out infinite; /* 动画名称 时长 缓动 重复 */
}

/* 加载动画 */
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

适用场景:加载动画、交互动效、注意力引导

17. 响应式图片

.responsive-image {
  max-width: 100%;    /* 最大宽度100% */
  height: auto;       /* 高度自动 */
  display: block;     /* 块级元素 */
}

/* 保持比例的容器 */
.aspect-ratio-box {
  width: 100%;
  height: 0;
  padding-bottom: 56.25%; /* 16:9比例 (9/16=0.5625) */
  position: relative;
  overflow: hidden;
}

.aspect-ratio-box img {
  position: absolute;
  width: 100%;
  height: 100%;
  object-fit: cover; /* 覆盖整个容器 */
}

适用场景:响应式网站、图片画廊、产品展示

18. 三角形绘制

.triangle-up {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid #3498db;
}

.triangle-down {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 100px solid #e74c3c;
}

.triangle-left {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-bottom: 50px solid transparent;
  border-right: 100px solid #2ecc71;
}

适用场景:工具提示、下拉箭头、装饰元素

19. 文字描边效果

.text-stroke {
  font-size: 3rem;
  font-weight: bold;
  color: white;
  -webkit-text-stroke: 2px #3498db; /* 描边宽度和颜色 */
  text-stroke: 2px #3498db;
}

/* 兼容写法 */
.text-stroke-alt {
  font-size: 3rem;
  font-weight: bold;
  color: transparent;
  -webkit-text-stroke: 2px #3498db;
  text-shadow: none;
}

适用场景:标题设计、海报文字、特色展示

20. 混合模式

.blend-mode {
  background-image: url('background.jpg');
  background-size: cover;
}

.blend-mode::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #3498db;
  mix-blend-mode: overlay; /* 叠加混合模式 */
}

/* 图片混合模式 */
.image-blend {
  background: 
    linear-gradient(45deg, #ff6b6b, #4ecdc4),
    url('image.jpg');
  background-blend-mode: overlay;
  background-size: cover;
}

适用场景:创意设计、图片处理、视觉效果


总结

这20个CSS技巧包含了现代最常见的开发需求,从布局到动画,从响应式到视觉效果。掌握这些技巧,你就能应对90%的日常开发需求。

其中有几点比较关键:

  1. 变量管理让维护更轻松
  2. Flex/Grid是布局的首选
  3. 过渡和动画提升用户体验

希望这些技巧能帮助你在开发中事半功倍!

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot+Vue3 整合 SSE 实现实时消息推送》

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

《SpringBoot 动态菜单权限系统设计的企业级解决方案》

《Vue3 + ElementPlus 动态菜单实现:一套代码完美适配多角色权限系统》

CSS 相对颜色:告别 180 个颜色变量的设计系统噩梦

作者 大知闲闲i
2025年11月19日 15:51

当组件库中的颜色变量达到 180 个时,一次品牌色变更就成了前端开发的噩梦。CSS 相对颜色语法将彻底改变这一现状。

一个让人沉默的现实

最近在排查一个组件库的主题 BUG 时,我们发现了令人震惊的事实:这个看似成熟的设计系统中,竟然定义了 180 个颜色变量

更可怕的是,每次品牌主色调整,都需要在 3 个不同的文件中同步修改 15 种深浅变化、hover 状态、透明度变体……设计同学轻描淡写的一句"主色想从偏蓝调成更紫一点",意味着工程侧需要:

  • 手动修改 15+ 个变量

  • 反复对比 hover、active 状态是否协调

  • 仔细检查半透明背景是否漏改

  • 确保整个颜色体系保持和谐

漏改一个变量,hover 状态显得怪异;漏改两个,整套主题就开始"发脏"。

传统颜色系统的困境

当前绝大多数设计系统的配色方案可以概括为:靠人肉复制的"颜色农场"

:root {
  /* 主色系 */
  --color-primary: #3b82f6;
  --color-primary-hover: #2563eb;
  --color-primary-active: #1d4ed8;
  --color-primary-light: #93c5fd;
  --color-primary-dark: #1e40af;
  
  /* 辅助色系 */
  --color-secondary: #8b5cf6;
  --color-secondary-hover: #7c3aed;
  --color-secondary-active: #6d28d9;
  
  /* 继续衍生... */
}

这种模式的痛点显而易见:

  • 维护成本高:一个主色系需要十几二十个变量

  • 同步困难:多处定义的变量容易遗漏

  • 不可靠:手动调色依赖个人感觉,缺乏系统性

CSS 相对颜色:革命性的解决方案

CSS 相对颜色语法引入的 from 关键字,让颜色从"死值"变成"活公式"。

基础语法

color-function(from origin-color channel1 channel2 channel3 / alpha)

拆解说明:

  • color-function:输出格式,如 rgb()hsl()oklch()

  • from:关键字符,声明颜色来源

  • origin-color:基准颜色,支持 hex、RGB、HSL 等格式

  • channel1 ~ 3:可访问和修改的通道值

  • alpha:可选透明度通道

实际应用示例

:root {
  --primary: #3b82f6;
}

.button {
  background: var(--primary);
}

.button:hover {
  /* 基于主色自动计算 hover 状态 */
  background: hsl(from var(--primary) h s calc(l - 10));
}

这一行 hsl(from ...) 的改变,将 hover 效果从"写死"变成了"相对基色、自动联动"。从此,品牌色只需修改一个 --primary 变量,所有衍生状态自动跟随。

from 关键字的魔力

from 的核心作用是将颜色分解为通道值,让我们能够像搭乐高一样重新组合:

/* 将绿色分解为 RGB 通道 */
rgb(from green r g b)  /* 输出: rgb(0 128 0) */

/* 用绿色通道创建灰度 */
rgb(from green g g g)  /* 输出: rgb(128 128 128) */

/* 随意调换通道顺序 */
rgb(from green b r g)  /* 输出: rgb(0 0 128) */

跨色彩空间转换

from 自动处理色彩空间转换,让颜色格式不再成为障碍:

/* RGB 转 HSL */
hsl(from rgb(255 0 0) h s l)

/* Hex 转 OKLCH */
oklch(from #3b82f6 l c h)

这对设计系统意义重大:源头存储格式不再重要,使用端始终使用统一的可计算空间。

calc():颜色计算的引擎

真正的威力在于将 calc() 与颜色通道结合:

/* 变亮:提高亮度 */
hsl(from blue h s calc(l + 20))

/* 变暗:降低亮度 */  
hsl(from blue h s calc(l - 20))

/* 半透明:调整透明度 */
rgb(from blue r g b / calc(alpha * 0.5))

/* 调色:旋转色相 */
hsl(from blue calc(h + 180) s l)

大部分颜色衍生逻辑都可以归结为:通道 + 偏移量通道 × 系数

OKLCH:更智能的色彩空间

虽然 HSL 很流行,但它有个致命缺陷:亮度感知不均

hsl(220 80% 50%)  /* 蓝色 */
hsl(120 80% 50%)  /* 绿色 */

理论上两者亮度相同,但人眼感知中绿色明显更亮。OKLCH 解决了这个问题:

  • L(Lightness):0-1,感知亮度,更符合人眼

  • C(Chroma):0-约0.37,颜色纯度

  • H(Hue):0-360,色相角度

    oklch(0.55 0.15 260) /* 蓝色 / oklch(0.55 0.15 140) / 绿色 */

在 OKLCH 中,相同的 L 值在不同色相间具有一致的亮度感知,这让程序化调色更加可靠。

构建智能颜色系统

第一步:定义品牌基色

:root {
  /* 只用定义 4 个基础品牌色 */
  --brand-primary: oklch(0.55 0.2 265);
  --brand-success: oklch(0.65 0.18 145);
  --brand-error: oklch(0.6 0.25 25);
  --brand-warning: oklch(0.75 0.15 85);
}

第二步:按规则生成完整色板

:root {
  /* Primary 色系 - 全部从基色派生 */
  --primary: var(--brand-primary);
  --primary-hover: oklch(from var(--brand-primary) calc(l - 0.1) c h);
  --primary-active: oklch(from var(--brand-primary) calc(l - 0.15) c h);
  --primary-light: oklch(from var(--brand-primary) calc(l + 0.2) calc(c * 0.5) h);
  --primary-lighter: oklch(from var(--brand-primary) calc(l + 0.3) calc(c * 0.3) h);
  --primary-alpha-10: oklch(from var(--brand-primary) l c h / 0.1);
  --primary-alpha-20: oklch(from var(--brand-primary) l c h / 0.2);
  
  /* 其他色系采用相同模式 */
  --success: var(--brand-success);
  --success-hover: oklch(from var(--brand-success) calc(l - 0.1) c h);
  --success-light: oklch(from var(--brand-success) calc(l + 0.2) calc(c * 0.5) h);
}

四个基色变量,扩展出完整的颜色体系。品牌色调整时,只需修改四个基础值。

暗色模式的革命

传统暗色模式需要维护两套 token,现在只需一个公式:

:root {
  --surface: oklch(0.98 0.02 240);
  --text: oklch(0.25 0.03 240);
}

[data-theme="dark"] {
  /* 亮度反转实现暗色模式 */
  --surface: oklch(from var(--surface) calc(1 - l) c h);
  --text: oklch(from var(--text) calc(1 - l) c h);
}

实战高级技巧

1. 智能阴影系统

.card {
  --card-bg: var(--primary);
  background: var(--card-bg);
  box-shadow: 
    0 4px 6px oklch(from var(--card-bg) l c h / 0.2),
    0 10px 15px oklch(from var(--card-bg) l c h / 0.15);
}

阴影自动适应背景色,主题切换时自然过渡。

2. 确保可读性的文本颜色

.tag {
  --tag-bg: var(--primary);
  background: var(--tag-bg);
  /* 文本亮度比背景高 0.6,确保对比度 */
  color: oklch(from var(--tag-bg) calc(l + 0.6) c h);
}

3. 品牌化半透明遮罩

.modal-backdrop {
  background: oklch(from var(--brand-primary) l c h / 0.7);
}

浏览器支持与渐进增强

截至 2025 年,相对颜色已获得良好支持:

  • Chrome 119+ ✅

  • Firefox 128+ ✅

  • Safari 16.4+ ✅

  • Edge 119+ ✅

覆盖率约 83%,对于不支持的环境可提供静态回退:

.button {
  background: #2563eb; /* 回退值 */
  background: oklch(from var(--primary) calc(l - 0.1) c h);
}

避坑指南

  1. 避免过深派生链:从基色直接推导,最多两层

  2. 控制 Chroma 范围:OKLCH 中 Chroma 超过 0.37 可能导致颜色溢出

  3. 正确使用 Alphaoklch(0.6 0.2 265 / 0.5) 而非 oklch(0.6 0.2 265 0.5)

总结:从小升级到大变革

CSS 相对颜色解决的不仅是技术问题,更是设计系统维护的哲学变革:

  • 主题切换不再是灾难:改一个变量,全站自洽

  • 颜色 Token 真正集中管理:从分散定义到统一源头

  • 设计规则化:深浅、状态、透明度都成为可复用的公式

  • 开发体验提升:从机械调色到智能推导

下一次重构配色系统时,不妨尝试将基准色、状态色、暗色模式、半透明层全部交给相对颜色计算。那种"改一处,全局联动"的流畅体验,确实让人上头。

从 180 个颜色变量到 4 个基色变量,这不只是数量的减少,更是设计系统维护理念的质的飞跃。

Gird快速入门(手把手敲demo)

作者 新晨437
2025年11月19日 21:02

Gird 布局是 CSS 中目前最强大、最成熟的二维布局方案,它可以让您轻松创建复杂的网页结构。

这份快速入门指南将带您从零开始,快速掌握 Grid 的核心概念和用法。

1. 什么是 CSS Grid?

CSS Grid Layout(网格布局)是一个二维的布局系统,意味着它可以同时处理。这与 Flexbox(一维布局,主要处理行列)形成鲜明对比。

核心思想:将一个容器划分为一个个网格,你可以将子元素放置在这些网格的任意位置,甚至可以创建复杂的、不对称的布局。


2. 基本概念

在开始之前,先了解两个核心概念:

  • Grid Container(网格容器):应用了 display: grid; 的元素。它的所有直接子元素将成为 Grid Item。
  • Grid Item(网格项目):Grid Container 的直接子元素
<div class="container"> <!-- Grid Container -->
  <div class="item">1</div> <!-- Grid Item -->
  <div class="item">2</div> <!-- Grid Item -->
  <div class="item">3</div> <!-- Grid Item -->
</div>

3. 快速启动:创建一个简单的网格

让我们通过一个简单的例子,一步步创建一个 3x3 的网格。

步骤 1:定义网格容器

首先,将一个元素的 display 属性设置为 gridinline-grid

.container {
  display: grid;
}

现在,.container 就成为了一个网格容器,它的直接子元素自动成为了网格项目。

步骤 2:划分行和列(定义网格轨道)

这是 Grid 布局的核心。我们使用 grid-template-columnsgrid-template-rows 属性来定义网格的结构。

  • grid-template-columns:定义每一列的宽度。
  • grid-template-rows:定义每一行的高度。

让我们定义一个 3 列(每列 100px)和 3 行(每行 100px)的网格:

.container {
  display: grid;
  grid-template-columns: 100px 100px 100px; /* 三列,每列100px */
  grid-template-rows: 100px 100px 100px;    /* 三行,每行100px */
}

更现代的写法:使用 repeat() 函数 当列或行很多时,repeat() 函数非常有用。

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px); /* 等同于 100px 100px 100px */
  grid-template-rows: repeat(3, 100px);
}

灵活的写法:使用 fr 单位 fr(fraction)单位代表网格容器中的等分空间,它让布局变得非常灵活。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr; /* 总宽度分为4份,第二列占2份,第一、三列各占1份 */
  grid-template-rows: repeat(3, 100px);
}

混合使用:

.container {
  display: grid;
  grid-template-columns: 200px 1fr 20%; /* 第一列固定200px,第三列是容器的20%,中间列占据剩余空间 */
}

步骤 3:网格间隙(Gap)

使用 gap 属性可以设置网格项目之间的间距。它是 row-gapcolumn-gap 的简写。

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  gap: 10px; /* 行和列的间隙都是10px */
  /* 或者分开写: */
  /* row-gap: 15px; */
  /* column-gap: 10px; */
}

到此为止,一个标准的 3x3 网格就创建好了!你的 HTML 和 CSS 看起来应该是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
  <style>
    .container {
      display: grid;
      grid-template-columns: repeat(3, 100px);
      grid-template-rows: repeat(3, 100px);
      gap: 10px;
      background-color: #f0f0f0;
      padding: 10px;
    }
    .item {
      background-color: #4CAF50;
      color: white;
      padding: 20px;
      text-align: center;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
    <div class="item">9</div>
  </div>
</body>
</html>

4. 高级操作:放置网格项目

默认情况下,项目会按顺序逐行逐列填充网格。但 Grid 的强大之处在于你可以精确控制每个项目的位置。

我们通过 网格线(Grid Lines) 来定位项目。网格线是划分网格的线,从 1 开始编号(也从 -1 开始倒序编号)。

CSS Grid Lines转存失败,建议直接上传图片文件

使用以下属性来控制项目位置:

  • grid-column-start
  • grid-column-end
  • grid-row-start
  • grid-row-end
  • 简写:grid-columngrid-row

示例:让第一个 item 横跨两列

.item:nth-child(1) {
  grid-column-start: 1;
  grid-column-end: 3; /* 从第1条纵线开始,到第3条纵线结束 */
  /* 简写: */
  /* grid-column: 1 / 3; */
}

示例:创建一个页面的经典布局

<div class="container">
  <header>Header</header>
  <aside>Sidebar</aside>
  <main>Main Content</main>
  <footer>Footer</footer>
</div>
.container {
  display: grid;
  grid-template-columns: 200px 1fr; /* 侧边栏固定宽度,主内容区自适应 */
  grid-template-rows: auto 1fr auto; /* 页头和页脚高度由内容决定,主内容区占据剩余空间 */
  height: 100vh; /* 让容器充满整个视口高度 */
  gap: 10px;
}

header {
  grid-column: 1 / -1; /* 从第1条纵线开始,到最后一条纵线结束(横跨所有列) */
  background: lightblue;
}

aside {
  grid-column: 1 / 2; /* 占据第一列 */
  background: lightcoral;
}

main {
  grid-column: 2 / 3; /* 占据第二列 */
  background: lightgreen;
}

footer {
  grid-column: 1 / -1; /* 横跨所有列 */
  background: lightgray;
}

5. 快速总结与最佳实践

  1. 启动display: grid;
  2. 定义结构
    • grid-template-columns: 定义列。
    • grid-template-rows: 定义行。
    • 善用 repeat()fr 单位。
  3. 设置间距gap
  4. 精细控制项目:使用 grid-columngrid-row 基于网格线进行定位。
  5. 命名网格线:对于复杂布局,可以给网格线起名字,让代码更易读。
    .container {
      grid-template-columns: [sidebar-start] 200px [sidebar-end content-start] 1fr [content-end];
    }
    header {
      grid-column: sidebar-start / content-end;
    }
    

练习建议

打开浏览器的开发者工具,在 ElementsInspector 面板中,找到你的网格容器,旁边会有一个 grid 的标签,点击它可以在页面上可视化你的网格结构。这是学习和调试 Grid 的绝佳方式!

Grid 布局功能非常丰富,还包括网格区域(grid-template-areas)、对齐属性(justify-items, align-content等),但掌握了以上核心内容,你已经可以应对 90% 的布局需求了。快去动手试试吧!

Gird布局详解

作者 嬉皮客
2025年11月18日 21:16

一、什么是 Grid 布局?

CSS Grid Layout(网格布局)是 CSS 提供的二维布局系统,可以同时在行(row)和列(column)两个方向上对元素进行排列。
它和 Flexbox 不同,Flexbox 是一维布局(只能按行或列排),而 Grid 是二维布局,可以轻松实现复杂的跨行、跨列布局。

特点:

  • 二维布局:同时控制行和列,对应一维布局复杂度也随之增加
  • 跨行跨列:元素可以跨多个单元格,一维度布局需要层层嵌套来做这些效果
  • 语义清晰grid-template-areas 像地图一样描述布局,布局就像ASCLL表一样清晰直观
  • 响应式友好:只改 Grid 定义,不动 HTML,就能重排布局,经常配合@media使用
  • 现代浏览器支持好:桌面端和移动端几乎全覆盖(IE11 部分支持旧语法),现代浏览器基本都是支持的,排除一下老旧项目(政府、银行等)还在使用

二、基本概念

容器和项目

什么是容器?

设置了 display: grid 或 display: inline-grid 的元素,就是 Grid 容器,容器的直接子元素就是 Grid 项目。容器负责定义整个网格的结构(行、列、间距、对齐方式等)。

什么是项目?

Grid 容器的直接子元素就是 Grid 项目,项目可以通过属性控制它在网格中的位置、跨行跨列、对齐方式等。

行和列

容器里面的水平区域称为"行"(row),垂直区域称为"列"(column)。

下图水平深色区域就是“行”,垂直深色区域就是“列”

image.png

单元格

行和列的交叉区域,称为"单元格"(cell),n行和m列会产生n x m个单元格。比如,3行3列会产生9个单元格。

网格线

划分网格的线,称为"网格线"(grid line)。水平网格线划分出行,垂直网格线划分出列,n行有n + 1根水平网格线,m列有m + 1根垂直网格线,比如三行就有四根水平网格线。

如下图就有5根水平网格线和5根垂直网格线

image.png

三、容器属性

display:grid | inline-grid

作用:指定一个容器采用网格布局

语法:

.container{
    display: grid | inline-grid;
}

注意:设为网格布局以后,容器子元素(项目)的floatdisplay: inline-blockdisplay: table-cellvertical-aligncolumn-*等设置都将失效。

grid-template-columns与grid-template-rows

作用:grid-template-columns属性定义每一列的列宽,grid-template-rows属性定义每一行的行高

实例:指定了一个三行三列的网格,列宽和行高都是100px

image.png

.container {
  display: grid;
  grid-template-columns: 100px 100px 100px;
  grid-template-rows: 100px 100px 100px;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>
repeat()

作用:使用repeat()函数,简化重复的值,接受两个参数,第一个参数是重复的次数,第二个参数是所要重复的值,也可以重复某个模式

实例:

  • 简单重复值

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
}
.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • 重复某个模式

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
}
.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
auto-fill与auto-fit

作用:单元格的大小是固定的,但是容器的大小不确定。如果希望每一行(或每一列)容纳尽可能多的单元格,这时可以使用auto-fill关键字表示自动填充,只有当容器足够宽,可以在一行容纳所有单元格,并且单元格宽度不固定的时候,才会有差异:auto-fill会用空格子填满剩余宽度,auto-fit则会尽量扩大单元格的宽度

实例:

例子中宽设置了400px,高设置200px,属性auto-fill,高400px/100px=4个格子,宽200px/100px=2个格子,所以超出的第九个被空放了

image.png

.container {
  width: 400px;
  height: 200px;
  display: grid;
  grid-template-columns: repeat(auto-fill, 100px);
  grid-template-rows: repeat(auto-fill, 100px);
}
fr

作用:为了方便表示比例关系,网格布局提供了fr关键字(fraction 的缩写,意为"片段")。如果两列的宽度分别为1fr2fr,就表示后者是前者的两倍

实例:

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: 300px 1fr 2fr;
  grid-template-rows: repeat(3, 100px);
}
minmax

作用:minmax()函数产生一个长度范围,表示长度就在这个范围之中。它接受两个参数,分别为最小值和最大值

实例:

每行第三列位置宽度100px~2fr

image.png

.container {
  display: grid;
  grid-template-columns: 1fr 1fr minmax(100px, 2fr);
  grid-template-rows: repeat(3, 100px);
}
auto

作用:由浏览器自己决定长度

实例:

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: 100px auto 100px;
  grid-template-rows: repeat(3, 100px);
}
自定义网格线名称

作用:指定每一根网格线的名字,方便以后的引用

实例:

指定网格布局为3*3,因此有4根垂直网格线和4根水平网格线,每个网格线可以指定多个名字,比如[r4 xipiker666]

.container {
  display: grid;
  grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4];
  grid-template-rows: [r1] 100px [r2] 100px [r3] auto [r4 xipiker666];
}

grid-columns-gap(columns-gap)与grid-rows-gap(rows-gap)与grid-gap(gap)

作用:grid-columns-gap设置列间距、grid-rows-gap设置行间距,可以分别简写为columns-gaprows-gapgrid-gap设置行列间距grid-gap: <行间距> <列间距>grid-gap可以简写为gap,如果行和列的值相等可以gap: xxpx

实例:

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  gap: 20px;
}

grid-template-areas

作用:网格布局允许指定"区域"(area),一个区域由单个或多个单元格组成

实例:

  • 划分出9个单元格
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a b c'
    'd e f'
    'g h i';                 
  gap: 20px;  
}
  • 多个单元格合并成一个区域
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a a a'
    'b b b'
    'c c c';                 
  gap: 20px;  
}
  • 某些区域不需要利用,则使用.表示
.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-template-areas: 
    'a . a'
    'b . b'
    'c . c';                 
  gap: 20px;  
}

grid-auto-flow

作用:容器的子元素的排序方式,属性值row先行后列,属性值columns先列后行,属性值row dense子项目指定位置后再先行后列排序,属性值columns dense子项目指定位置后再先列后行排序

实例:

  • row先行后列

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: row;
}
  • columns先列后行

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: column;
}
  • row dense子项目指定位置后先行后列

image.png

.container {
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: row dense;
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.item_two {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • columns dense子项目指定位置后先列后行

image.png

.container {
  width: 400px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  grid-auto-flow: column dense;
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.item_two {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-items与align-items与place-items

作用:justify-items属性设置单元格内容的水平位置,align-items属性设置单元格内的垂直位置,place-items属性是justify-itemsalign-items的组合属性

实例:

  • justify-items: start | center | end | stretch

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  justify-items: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • align-items: start | center | end | stretch

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  align-items: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • place-items: <align-items> <justify-items>

image.png

.container {
  width: 300px;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  place-items: center end;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-content与align-content与place-content

作用:justify-content属性设置内容区域在容器里面的水平位置,align-content属性设置内容区域在容器里面的垂直位置,place-content属性是justify-contentalign-content的组合属性

实例:

  • justify-content: start | end | center | stretch | space-around | space-between | space-evenly

image.png

.container {
  width: 600px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  justify-content: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • align-content: start | end | center | stretch | space-around | space-between | space-evenly

image.png

.container {
  width: 600px;
  height: 400px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  align-content: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • place-content: <align-content> <justify-content>

image.png

.container {
  width: 600px;
  height: 400px;
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 100px);
  grid-template-rows: repeat(3, 100px);
  place-content: center center;
}

grid-auto-columns与grid-auto-rows

作用:可以理解为,现有网格的外部设置多余的网格,用来放超出的子项目

实例:

  • grid-auto-columns

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-columns: 100px 100px;
  grid-template-rows: 50px;
  grid-auto-rows: 80px
  grid-auto-flow: row; /*这里要设置按照行方向填充,可以不设置,因为默认按照行方向*/
}
  • grid-auto-rows

image.png

.container{
  background: #f9f9f9;
  display: grid;
  grid-template-rows: 50px 50px;
  grid-template-columns: 100px;
  grid-auto-columns: 150px; 
  grid-auto-flow: column; /*注意这里要设置按照列方向填充,必须设置,因为默认按照行防线*/
}

grid-template与grid

作用:grid-template属性是grid-template-columnsgrid-template-rowsgrid-template-areas这三个属性的合并简写形式,grid属性是grid-template-rowsgrid-template-columnsgrid-template-areas、 grid-auto-rowsgrid-auto-columnsgrid-auto-flow这六个属性的合并简写形式

四、项目属性

grid-column-start、grid-column-end与grid-row-start、grid-row-end

作用:可以理解为设置项目占多少个网格线,分别定位在哪根网格线

实例:

  • grid-column-start: <number>左边框所在的垂直网格线、grid-column-end: <number>右边框所在的垂直网格线

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-column-start: 1;
  grid-column-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div className="item_first">1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>
  • grid-row-start: <number>上边框所在的垂直网格线、grid-row-end: <number>下边框所在的垂直网格线

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-row-start: 1;
  grid-row-end: 3;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div class="container">
    <div className="item_first">1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
    <div>5</div>
    <div>6</div>
    <div>7</div>
    <div>8</div>
    <div>9</div>
</div>

grid-column与grid-row

作用:grid-column属性是grid-column-startgrid-column-end的简写形式、grid-row属性是grid-row-startgrid-row-end的简写形式

实例:

  • grid-column: <grid-column-start> / <grid-column-end>
  • grid-row: <grid-row-start> / <grid-row-end>

grid-area

作用:指定项目放在哪个区域,grid-area属性还可用作grid-row-startgrid-column-startgrid-row-endgrid-column-end的合并简写形式,直接指定项目的位置

实例:

  • 指定放在哪个区域

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
  grid-template-areas:
    'a b c'
    'd e f'
    'g h i';
}

.item_first {
  grid-area: e;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
  • grid-area: <row-start> / <column-start> / <row-end> / <column-end>

image.png

@pos: ~'1 / 1 / 3 / 3';

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  grid-area: @pos;
}

.container > div {
  width: 100%;
  height: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

justify-self与align-self与place-self

作用:justify-self属性设置单元格内容的水平位置、align-self属性设置单元格内容的垂直位置,place-self属性是justify-selfalign-self的属性组合

实例:

image.png

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-rows: repeat(3, 100px);
  grid-template-columns: repeat(3, 100px);
}

.item_first {
  justify-self: center;
  align-self: center;
}

.container > div {
  width: 60px;
  height: 60px;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}

五、常见布局案例

三列等宽布局

效果

image.png

代码

.container {
  background: #f9f9f9;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.container > div {
  width: 100%;
  border: 1px solid #ccc;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 14px;
  font-weight: bold;
}
<div className="container">
    <div>1</div>
    <div>2</div>
    <div>3</div>
</div>

圣杯布局

效果

image.png

代码

.container {
  display: grid;
  grid-template-areas:
    'header header'
    'sidebar main'
    'footer footer';
  grid-template-columns: 200px 1fr;
  grid-template-rows: 60px 1fr 40px;
  gap: 10px;
}
.header {
  grid-area: header;
  background: lightblue;
}
.sidebar {
  grid-area: sidebar;
  background: lightgoldenrodyellow;
}
.main {
  grid-area: main;
  background: lightgreen;
}
.footer {
  grid-area: footer;
  background: lightpink;
}
<div class="container">
    <div className="header">header</div>
    <div className="sidebar">sidebar</div>
    <div className="main">main</div>
    <div className="footer">footer</div>
</div>

媒体查询响应式布局(Gird+Flex最佳拍档)

效果

屏幕宽度小于等于660px

image.png

屏幕宽度小于等于1024

image.png

屏幕宽度大于1024

image.png代码

<div class="dashboard">
    <div class="stats">Stats</div>
    <div class="overview">Overview</div>
    <div class="lifecycle">Lifecycle</div>
    <div class="calendar">Calendar</div>
    <div class="monitor">Monitor</div>
    <div class="ladder">Ladder</div>
    <div class="stage">Stage</div>
</div>
.dashboard {
  display: grid;
  gap: 20px;
  padding: 20px;
  /* 桌面端布局 */
  grid-template-columns: 2fr 2fr 1fr;
  grid-template-areas:
    'stats stats stats'
    'overview lifecycle calendar'
    'monitor ladder calendar'
    'stage stage stage';
}

.dashboard > div {
  background: #eee;
  padding: 10px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  font-weight: bold;
}

.stats {
  grid-area: stats;
}
.overview {
  grid-area: overview;
}
.lifecycle {
  grid-area: lifecycle;
}
.calendar {
  grid-area: calendar;
}
.monitor {
  grid-area: monitor;
}
.ladder {
  grid-area: ladder;
}
.stage {
  grid-area: stage;
}

@media (max-width: 1024px) {
  .dashboard {
    grid-template-columns: 1fr 1fr;
    grid-template-areas:
      'stats stats'
      'overview lifecycle'
      'calendar calendar'
      'monitor ladder'
      'stage stage';
  }
}

@media (max-width: 600px) {
  .dashboard {
    grid-template-columns: 1fr;
    grid-template-areas:
      'stats'
      'overview'
      'lifecycle'
      'calendar'
      'monitor'
      'ladder'
      'stage';
  }
}

Grid瀑布流布局

效果

image.png

代码

.container {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-auto-rows: 10px;
  grid-auto-flow: dense;
  gap: 10px;
}
.item {
  background: lightblue;
}
.item1 {
  grid-row-end: span 15;
}
.item2 {
  grid-row-end: span 25;
}
.item3 {
  grid-row-end: span 20;
}
<div class="container">
    <div class="item item1">1</div>
    <div class="item item2">2</div>
    <div class="item item3">3</div>
    <div class="item item1">4</div>
</div>

六、总结

上述内容主要参考,大家可以参考原文,如果能帮到您,欢迎点赞、收藏、+关注

CSS Grid 网格布局教程 - 阮一峰的网络日志

element-plus主题配置及动态切换主题

作者 天外来物
2025年11月18日 14:02

创建vue项目的不同方式

pnpm create vite 使用于创建一些组件库,第三方库的时候

pnpm create vue 适用于vue项目,内部有一些基础的样式,vue的主题,

element-plus主题配置

按需引入element

1. scss变量自定义主题

分支:feature-element-theme-anxu-scss

src/styles/element/index.scss

/* just override what you need */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': green,
    ),
  )
);

vite.config.ts

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

按需引入组件时,使用组件,会自动引入对应组件的样式,不用再main.ts中引入elememnt的全部样式。

主题配置只需要修改需要改变的颜色变量即可

可以看到,对应组件的颜色变量已经变化

image.png

2. css变量自定义主题

分支: feature-element-theme-anxu-css

src/styles/element/index.scss

:root {
  --el-color-primary: green;
}

vite.config.ts

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

可以看到,css变量被注入到组件对应的scss文件顶部,从而让主题生效

image.png

全量引入element

scss全量引入

分支 feature-element-theme-all-scss

/* just override what you need */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': green,
    ),
  )
);

// If you just import on demand, you can ignore the following content.
// 如果你是全量导入,需要加下边这句,如果是按需引入,请注释掉下边这句
@use 'element-plus/theme-chalk/src/index.scss' as *;

main.ts

import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import './styles/element/index.scss'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.use(ElementPlus)
app.mount('#app')

这时候可以看到样式被全部引入了

image.png

css全量引入

分支:feature-element-theme-all-css src/styles/element/index.css

:root {
  --el-color-primary: green;
}

main.ts

import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './styles/element/index.css'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.use(ElementPlus)
app.mount('#app')

可以看到,element的css变量已经被覆盖了

image.png

动态主题

运行时动态切换主题,不能依赖scss变量

原因

SCSS 变量是编译时变量,在构建阶段就被替换掉了,在浏览器运行时无法再改变,所以无法用于“动态切换主题”。

动态主题应该使用css变量来实现

官方提供了一个切换主题的项目github.com/element-plu…

里面引入了一套暗黑模式的样式,然后通过 useToggle进行切换

分支 feature-element-theme-anxu-scss-dynamic 还是采用按需引入elememnt的方式

src/styles/index.scss 这里全量引入暗黑模式的样式

// import dark theme
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;

// :root {
//   --ep-color-primary: red;
// }

body {
  font-family:
    Inter, system-ui, Avenir, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
    'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0;
}

a {
  color: var(--ep-color-primary);
}

main.ts在main.ts中引入样式

// import './assets/main.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import '@/styles/index.scss'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

src/styles/element/index.scss 这里是自定义默认主题变量

$--colors: (
  'primary': (
    'base': rgb(0, 128, 19),
  ),
  'success': (
    'base': #21ba45,
  ),
  'warning': (
    'base': #f2711c,
  ),
  'danger': (
    'base': #db2828,
  ),
  'error': (
    'base': #db2828,
  ),
  'info': (
    'base': #42b8dd,
  ),
);

// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  // do not use same name, it will override.
  $colors: $--colors // $button-padding-horizontal: ('default': 50px)
);

// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;

// You can comment it to hide debug info.
// @debug $--colors;

// custom dark variables
@use './dark.scss';

src/styles/element/dark.scss 这是对暗黑主题颜色重新定义

// only scss variables

$--colors: (
  'primary': (
    'base': #589ef8,
  ),
);

@forward 'element-plus/theme-chalk/src/dark/var.scss' with (
  $colors: $--colors
);

vite.config.ts 这里引入自定义主题变量,自定义默认主题和暗黑主题颜色

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import ElementPlus from 'unplugin-element-plus/vite'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [
        ElementPlusResolver({
          importStyle: 'sass',
        }),
      ],
    }),
    ElementPlus({
      useSource: true,
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/element/index.scss" as *;`,
      },
    },
  },
})

动态切换主题

  • 通过useDark获取当前是否是暗黑模式
  • 通过useToggle切换主题
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
const changeTheme = () => {
  toggleDark()
}
</script>

<template>
  <header>
    <el-button type="primary" @click="changeTheme"> 切换主题 </el-button>
    <el-input></el-input>
  </header>

  <RouterView />
</template>

<style scoped>
header {
  line-height: 1.5;
  max-height: 100vh;
}

.logo {
  display: block;
  margin: 0 auto 2rem;
}

nav {
  width: 100%;
  font-size: 12px;
  text-align: center;
  margin-top: 2rem;
}

nav a.router-link-exact-active {
  color: var(--color-text);
}

nav a.router-link-exact-active:hover {
  background-color: transparent;
}

nav a {
  display: inline-block;
  padding: 0 1rem;
  border-left: 1px solid var(--color-border);
}

nav a:first-of-type {
  border: 0;
}

@media (min-width: 1024px) {
  header {
    display: flex;
    place-items: center;
    padding-right: calc(var(--section-gap) / 2);
  }

  .logo {
    margin: 0 2rem 0 0;
  }

  header .wrapper {
    display: flex;
    place-items: flex-start;
    flex-wrap: wrap;
  }

  nav {
    text-align: left;
    margin-left: -1rem;
    font-size: 1rem;

    padding: 1rem 0;
    margin-top: 1rem;
  }
}
</style>

代码仓库地址 github.com/Stacey1018/…

❌
❌