第五篇_构建真实页面_组件_响应式_维护性

第五篇 构建真实页面:组件、响应式、维护性

目标:把前面学到的盒模型、布局、视觉、动效,真正落地到"真实项目"的代码里。

看完这一篇,你应该能:

  • 用「组件化」的思维组织 CSS,不再是一堆散乱的选择器;
  • 为页面设计一套清晰的响应式策略,兼顾 PC 和移动端;
  • 使用 CSS 变量和 calc 做主题、间距、尺寸等的集中管理,为后面的架构篇打基础。

第12章 CSS 组件化思想

这一章解决的核心问题:

  • 写 CSS 时,如何避免"越写越乱、全站一起牵一发动全身"?
  • BEM 这种命名方式到底解决了什么问题?
  • 一个按钮、卡片、Badge 等组件的 CSS 应该长什么样?
  • 如何从"单页样式"走向"可复用组件库"?

12.1 BEM 命名规范

BEM 是前端界非常流行的一套命名约定,它的全称是:

  • B:Block(块)
  • E:Element(元素)
  • M:Modifier(修饰)
12.1.1 基本理念

你可以把 BEM 理解成:

  • Block:一个相对独立的"模块/组件"(如 cardbuttonnav
  • Element:组件内部的组成部分(如 card__titlecard__content
  • Modifier:在"基础样式"上做的变化(如 card--primarycard--highlight

典型命名规则:

txt 复制代码
.block
.block__element
.block--modifier

示意图:

txt 复制代码
card(Block)

┌────────────────────┐
│ card__header       │  ← Element
│───────────┐        │
│ card__title│       │
│───────────┘        │
│ card__content      │  ← Element
└────────────────────┘

card--primary(Modifier)

在 card 的基础上追加一些外观差异,如边框颜色、背景色等。
12.1.2 BEM 的实际写法

HTML:

html 复制代码
<article class="card card--primary">
  <header class="card__header">
    <h2 class="card__title">专业版</h2>
  </header>
  <div class="card__content">
    高级功能、优先支持、更多示例。
  </div>
</article>

CSS:

css 复制代码
/* Block: 卡片基础样式 */
.card {
  border-radius: 12px;                           /* 圆角边框 */
  padding: 16px 20px;                            /* 内边距:上下16px,左右20px */
  background: #ffffff;                           /* 白色背景 */
  box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06); /* 阴影:X偏移0 Y偏移10px 模糊30px */
}

/* Element: 卡片内的头部区域 */
.card__header {
  margin-bottom: 8px;                            /* 底部外边距8px */
}

/* Element: 卡片内的标题 */
.card__title {
  font-size: 1.1rem;                             /* 字号:1.1倍根字号 */
  font-weight: 600;                              /* 字重:半粗体 */
  color: #0f172a;                                /* 深色文字 */
}

/* Element: 卡片内的内容 */
.card__content {
  font-size: 0.95rem;                            /* 字号:略小于正文 */
  color: #475569;                                /* 灰色文字 */
}

/* Modifier: 主要样式的卡片变体 */
.card--primary {
  border: 1px solid #2563eb;                      /* 蓝色边框 */
  box-shadow: 0 12px 40px rgba(37, 99, 235, 0.18); /* 更强的蓝色阴影 */
}

可以看到:

  • .card 提供基础样式
  • .card__* 只在 card 内部使用,避免了"全局 class 撞名"的问题
  • .card--primary 只做"差异化",不重写整套样式
12.1.3 BEM 带来的好处
  • 可读性高:一眼就知道某个类属于哪个组件
  • 可维护性强:改动一个组件的样式,不容易影响其它组件
  • 可复用性好:一个 block 可以在多个页面重复使用

简单对比:

txt 复制代码
传统杂乱命名:

.title {}
.big-title {}
.box {}
.box2 {}
.new-box {}

BEM:

.hero {}
.hero__title {}
.hero__subtitle {}

.card {}
.card__title {}
.card__content {}
.card--primary {}

后者更适合团队合作和长期维护。

不必教条地"100% 按 BEM 写",但学会 BEM 的思路,会大大提升你组织 CSS 的能力。

12.2 模块化样式组织

除了命名规范,还需要考虑"文件怎么拆"。

12.2.1 避免 giant.css

很多项目早期只有一个 style.css,后来越写越多,最后变成几千上万行的大文件:

txt 复制代码
styles/
  └── style.css   // 所有东西都堆在这里

问题:

  • 找东西困难:修改某个按钮样式,要在大文件里到处搜索
  • 易产生"重复样式":忘了之前写过类似代码,又写一遍
  • 合作困难:多人同时改一个文件,冲突频繁
12.2.2 更合理的拆分方式

一种常见的拆分结构(示意):

txt 复制代码
styles/
  base/           # 基础样式(全局)
    _reset.css
    _typography.css
    _variables.css
  components/     # 组件(可复用)
    _button.css
    _card.css
    _badge.css
  layout/         # 页面级布局
    _header.css
    _footer.css
    _grid.css
  pages/          # 页面独有样式
    home.css
    pricing.css
  • base/:全局通用的基础设置
  • components/:可复用组件
  • layout/:布局结构(header、footer、grid 等)
  • pages/:特定页面独有的样式(仅少量)

在构建工具(如 webpack、Vite)或预处理器(如 Sass)里,可以把这些文件再汇总打包。

这里先建立"模块化思维"的概念,具体架构模式(ITCSS、SMACSS 等)会在第六篇再展开。

12.3 可复用组件:卡片、按钮、Badge

这一节我们通过三个最常见的 UI 元素,感受一下"组件化 CSS"的写法:

  • Card(卡片)
  • Button(按钮)
  • Badge(标记)
12.3.1 Button:基础 + 状态 + 尺寸

典型结构:

txt 复制代码
.btn           # 基础按钮样式
.btn--primary  # 主按钮
.btn--outline  # 线框按钮
.btn--sm       # 小号尺寸
.btn--lg       # 大号尺寸

示例:

css 复制代码
/* 按钮基础样式(Block) */
.btn {
  /* 布局相关 */
  display: inline-flex;           /* 行内弹性盒,可并排显示 */
  align-items: center;            /* 垂直居中对齐 */
  justify-content: center;        /* 水平居中对齐 */
  gap: 0.4em;                    /* 子元素间距(图标和文字) */
  
  /* 尺寸与间距 */
  padding: 0.6em 1.4em;          /* 内边距:em单位随字号缩放 */
  
  /* 视觉样式 */
  border-radius: 999px;           /* 超大圆角 = 胶囊形按钮 */
  border: 1px solid transparent;  /* 透明边框,为outline变体预留 */
  
  /* 字体样式 */
  font-size: 0.95rem;            /* 略小于正文的字号 */
  font-weight: 500;              /* 中等字重 */
  
  /* 交互相关 */
  cursor: pointer;               /* 鼠标指针变手型 */
  
  /* 过渡动画:三个属性同时过渡 */
  transition: background-color 0.15s ease,  /* 背景色过渡 */
              box-shadow 0.15s ease,        /* 阴影过渡 */
              transform 0.12s ease;         /* 变换过渡(更快) */
}

/* 主按钮样式(Modifier) */
.btn--primary {
  background: #2563eb;                       /* 蓝色背景 */
  color: #ffffff;                           /* 白色文字 */
  box-shadow: 0 10px 30px rgba(37, 99, 235, 0.35); /* 蓝色阴影 */
}

/* 主按钮悬停状态 */
.btn--primary:hover {
  background: #1d4ed8;                       /* 更深的蓝色 */
  box-shadow: 0 14px 40px rgba(37, 99, 235, 0.45); /* 更强的阴影 */
}

/* 线框按钮样式(Modifier) */
.btn--outline {
  background: transparent;                    /* 透明背景 */
  color: #2563eb;                           /* 蓝色文字 */
  border-color: rgba(37, 99, 235, 0.6);     /* 半透明蓝色边框 */
}

/* 小号按钮(Modifier - 尺寸变体) */
.btn--sm {
  padding: 0.35em 0.9em;                    /* 更小的内边距 */
  font-size: 0.85rem;                       /* 更小的字号 */
}

/* 大号按钮(Modifier - 尺寸变体) */
.btn--lg {
  padding: 0.8em 1.9em;                     /* 更大的内边距 */
  font-size: 1.05rem;                       /* 更大的字号 */
}

HTML:

html 复制代码
<button class="btn btn--primary btn--lg">立即开始</button>
<button class="btn btn--outline btn--sm">了解更多</button>

组件化的关键:不要每个按钮都写一套样式,而是把"通用部分"抽到 .btn,差异放到 modifier 上。

12.3.2 Card:可容纳任意内容的容器

卡片更像是一个"内容容器":

css 复制代码
/* 卡片容器基础样式 */
.card {
  border-radius: 16px;                        /* 中等圆角 */
  padding: 20px 24px;                         /* 内边距:上下20px,左右24px */
  background: #ffffff;                        /* 白色背景 */
  box-shadow: 0 12px 34px rgba(15, 23, 42, 0.08); /* 柔和阴影效果 */
}

/* 柔和背景的卡片变体 */
.card--muted {
  background: #f8fafc;                        /* 浅灰色背景 */
}

/* 卡片标题元素 */
.card__title {
  font-size: 1.1rem;                          /* 比正文稍大的字号 */
  font-weight: 600;                           /* 半粗体 */
  color: #0f172a;                            /* 深色标题 */
  margin-bottom: 0.35rem;                     /* 底部间距 */
}

/* 卡片元信息(如标签、分类) */
.card__meta {
  font-size: 0.75rem;                         /* 小号字体 */
  text-transform: uppercase;                   /* 转换为大写字母 */
  letter-spacing: 0.08em;                     /* 字母间距:增加可读性 */
  color: #64748b;                             /* 中等灰色 */
  margin-bottom: 0.75rem;                     /* 底部间距 */
}

/* 卡片内容文本 */
.card__content {
  font-size: 0.95rem;                         /* 略小于正文 */
  color: #475569;                             /* 深灰色文字 */
}

HTML:

html 复制代码
<article class="card card--muted">
  <div class="card__meta">SUGGESTED PLAN</div>
  <h3 class="card__title">团队协作版</h3>
  <p class="card__content">适合小团队的项目管理与协作,内置多种布局模板。</p>
</article>
12.3.3 Badge:用于状态与标签

Badge 是一种小体积的状态/标签标记:

css 复制代码
/* Badge 标记基础样式 */
.badge {
  display: inline-flex;                        /* 行内弹性盒 */
  align-items: center;                        /* 垂直居中 */
  padding: 0.1rem 0.55rem;                    /* 紧凑的内边距 */
  border-radius: 999px;                        /* 完全圆角(药丸形) */
  font-size: 0.75rem;                         /* 小号字体 */
  font-weight: 500;                           /* 中等字重 */
}

/* 成功状态的徽章 */
.badge--success {
  background: #dcfce7;                         /* 浅绿色背景 */
  color: #15803d;                             /* 深绿色文字 */
}

/* 警告状态的徽章 */
.badge--warning {
  background: #fef3c7;                         /* 浅黄色背景 */
  color: #92400e;                             /* 深棕色文字 */
}

/* 线框样式的徽章 */
.badge--outline {
  background: transparent;                      /* 透明背景 */
  border: 1px solid currentColor;              /* currentColor = 继承文字颜色 */
}

HTML:

html 复制代码
<span class="badge badge--success">已激活</span>
<span class="badge badge--warning badge--outline">试用中</span>

这些组件可以放在 components/ 目录中,在多个页面反复使用。

12.4 设计系统的基础思想

当你有了足够多的组件以后,就会进入"设计系统(Design System)"的范畴。

12.4.1 从"零散组件"到"系统化"

一开始你可能只有:按钮 + 卡片 + 标题。

逐渐你会抽象出:

  • 色彩系统(主色、成功、警告、背景色等)
  • 字体系统(字号层级、字重、行高)
  • 间距系统(统一的 4/8/12/16/24 间距刻度)
  • 组件库(Button、Input、Card、Modal、Toast...)

可以用一张结构图来理解:

txt 复制代码
设计系统

┌───────────────┐
│  基础令牌     │  color / spacing / typography / radius / shadow
├───────────────┤
│  组件         │  Button / Input / Card / Badge / Modal ...
├───────────────┤
│  模板 & 页面  │  登录页、列表页、详情页、仪表盘 ...
└───────────────┘

CSS 层面,你可以:

  • 把颜色、间距、字号等固化为 CSS 变量(下一章会展开)
  • 组件引用这些变量,而不是写死具体数值
12.4.2 初学者要做到哪一步?

在你刚开始写 CSS 的阶段,不需要追求完美的设计系统,但可以尽早养成两个习惯:

  1. 用组件的视角看页面

    • 看到一个页面时,先问:这里面有哪些"可复用组件"?
    • 比如:卡片、列表项、按钮、标签、导航、页脚等
  2. 减少复制粘贴

    • 当你发现自己在多个地方写了类似结构的 HTML/CSS,就考虑抽成一个组件类
    • 例如 cardbtn 这些基础组件

本章的目标,是让你从"给页面上色"升级到"给组件上色"。接下来第13章,我们会在组件之上,讨论不同屏幕下它们应该如何"变形",也就是响应式与适配。

第13章 响应式与适配

这一章解决的核心问题:

  • 如何让同一套页面在手机和电脑上都好用?
  • 媒体查询(@media)应该怎么写,按什么断点来划分?
  • 移动端布局的常见策略有哪些?
  • 字体和容器如何做自适应?REM + vw 的组合方案如何选择?

13.1 媒体查询基础

媒体查询允许你根据「设备特性」(如宽度、高度、像素密度)来写不同的 CSS。

最常用的是按视口宽度(width)切换布局:

css 复制代码
/* 默认样式:桌面端优先 */
.layout {
  max-width: 1120px;     /* 最大宽度:限制内容区域不超过1120px */
  margin: 0 auto;        /* 水平居中:上下0,左右自动 */
  padding: 24px 32px;    /* 内边距:上下24px,左右32px */
}

/* @media:媒体查询,根据设备特性应用不同样式 */
/* max-width: 768px 意思是:当视口宽度 ≤ 768px 时 */
@media (max-width: 768px) {
  .layout {
    padding: 16px;       /* 小屏下减小内边距,节省空间 */
                        /* 覆盖了上面的 24px 32px */
  }
}

理解方式:

txt 复制代码
当视口宽度 ≤ 768px 时,激活 @media 块中的规则,覆盖默认样式。
13.1.1 常见断点的思路

断点没有唯一标准,不同设计系统会有自己一套,但常见区间大致是:

css 复制代码
/* 常用断点参考 */
/* 小手机 */
@media (max-width: 640px) {     /* 宽度 ≤ 640px */
  /* iPhone SE、小屏安卓手机 */
}

/* 普通手机/小平板 */
@media (max-width: 768px) {     /* 宽度 ≤ 768px */
  /* iPhone 6/7/8/X、大部分安卓手机 */
}

/* 平板/小笔记本 */
@media (max-width: 1024px) {    /* 宽度 ≤ 1024px */
  /* iPad、Surface、小尺寸笔记本 */
}

/* 桌面端 */
@media (min-width: 1024px) {    /* 宽度 ≥ 1024px */
  /* 桌面显示器、大屏笔记本 */
}

更重要的是:断点应该跟"设计发生明显变化"的地方对齐,而不是盲目抄别人的数值。

一个实用的做法是:

  1. 先在浏览器中缩放窗口
  2. 当你观察到某个组件「开始看起来不舒服」时
  3. 在那个宽度附近设置断点

13.2 移动端布局策略

移动端布局不只是"缩小"桌面布局,而是有自己的一套优先级和交互习惯。

13.2.1 Mobile First vs Desktop First

两种思路:

  • Desktop First :先写桌面样式,再通过 @media (max-width: ...) 针对小屏做调整
  • Mobile First :先写手机样式,再通过 @media (min-width: ...) 为大屏「增强」

示意:

txt 复制代码
Desktop First:

默认:桌面 → max-width:768 改为移动

Mobile First:

默认:移动 → min-width:768 增强为平板/桌面

现在更推荐 Mobile First:

  • 代码往往更简洁:默认样式就是小屏,不用写很多覆盖规则
  • 与"渐进增强"的理念吻合:在能力更强的设备上让体验更好
13.2.2 常见布局模式的适配

举几个常见例子:

  1. 两栏布局(左图右文)
    • 桌面:左右分栏
    • 手机:上下堆叠
css 复制代码
/* 两栏布局:桌面端左右分栏 */
.feature {
  display: grid;
  grid-template-columns: 1.2fr 1fr;  /* 左侧列更宽(1.2:1比例) */
  gap: 32px;                         /* 列间距32px */
}

/* 手机端:改为上下堆叠 */
@media (max-width: 768px) {
  .feature {
    grid-template-columns: 1fr;      /* 单列布局 */
                                     /* gap继续生效,变成上下间距 */
  }
}
  1. 多列卡片网格
    • 桌面:3~4 列
    • 平板:2 列
    • 手机:1 列

可以直接使用前面学过的自适应写法:

css 复制代码
/* 自适应网格:根据容器宽度自动调整列数 */
.card-grid {
  display: grid;
  
  /* repeat(auto-fit, ...): 自动填充可用空间 */
  /* minmax(220px, 1fr): 每列最小220px,最大平分剩余空间 */
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  
  gap: 20px;                          /* 网格间距 */
}

/* 效果:
   - 宽屏:可能显示4-5列
   - 中屏:可能显示2-3列
   - 窄屏:自动变成单列
   无需写媒体查询!
*/

这种写法往往不需要太多媒体查询,根据容器宽度自动调整列数。

13.2.3 触控友好的尺寸

移动端布局还要考虑"手指点击区域":

  • 一般推荐:点击目标至少 40px x 40px
  • 按钮内边距充足,避免太小难点

你可以为 .btn 在小屏上略微增大 padding:

css 复制代码
/* 移动端触控优化 */
@media (max-width: 640px) {
  .btn {
    padding: 0.75em 1.6em;   /* 增大内边距,便于手指点击 */
    min-height: 44px;        /* 最小高度:iOS推荐44px */
                            /* 确保手指可以准确点击 */
  }
  
  /* 链接也需要增大点击区域 */
  a {
    padding: 0.5em;          /* 给链接加些内边距 */
    display: inline-block;   /* 让padding生效 */
  }
}

13.3 字体与容器自适应

响应式不仅是"布局变形",还包含字体、间距等的适配。

13.3.1 使用 REM 做整体缩放

回顾:

  • 1rem = 根元素 htmlfont-size

一个常见做法:

css 复制代码
/* REM 响应式方案:通过改变根字号实现整体缩放 */

html { 
  font-size: 16px;           /* 基础字号:1rem = 16px */
}

/* 小手机屏幕 */
@media (max-width: 640px) {
  html { 
    font-size: 15px;         /* 1rem = 15px,整体缩小93.75% */
  }
}

/* 更小屏幕 */
@media (max-width: 480px) {
  html { 
    font-size: 14px;         /* 1rem = 14px,整体缩小87.5% */
  }
}

/* 使用rem单位的元素会自动缩放:
   h1 { font-size: 2rem; }  
   桌面端:2 × 16 = 32px
   小手机: 2 × 14 = 28px
*/

此时:

  • 用 rem 定义的字号、间距会随着屏幕变窄轻微缩小
  • 用 px 定义的细节(如边框)保持不变
13.3.2 clamp() 做"自适应但有边界"的字体

clamp(min, preferred, max) 可以让值在一个范围内随视口变化,但不超出边界。

示例:

css 复制代码
/* clamp() 函数:响应式但有边界 */
.hero-title {
  /* clamp(最小值, 首选值, 最大值) */
  font-size: clamp(24px, 4vw, 40px);
  /*               ↑      ↑     ↑
     最小24px  优先用视口宽度4%  最大40px
     
     工作原理:
     - 视口宽600px:4vw = 24px(恰好最小值)
     - 视口宽800px:4vw = 32px(使用4vw)
     - 视口宽1000px:4vw = 40px(达到最大值)
     - 视口宽1200px:仍然是40px(不再增大)
  */
}

/* 其他常用的clamp用法 */
.container {
  width: clamp(320px, 90%, 1200px);   /* 容器宽度:最小320px,最大1200px */
  padding: clamp(1rem, 3vw, 3rem);    /* 内边距响应式 */
}

理解成:

txt 复制代码
最小 24px,最大 40px
中间区间按 4vw 随视口宽度变化

这样在小手机上不会太大,在超宽屏上也不会夸张地巨大。

13.3.3 容器宽度与安全区域

在大屏上不要让正文跨越整个视口宽度:

css 复制代码
/* 内容容器:限制最大宽度,提高阅读体验 */
.article-container {
  max-width: 680px;      /* 文章最大宽度,避免一行太长 */
  margin: 0 auto;        /* 水平居中 */
  padding: 0 16px;       /* 左右内边距,避免贴边 */
}

/* 为什么限制宽度?
   - 一行文字超过75个字符会难以阅读
   - 眼睛需要大幅度移动,容易丢失位置
   - 680px 约 = 45-75个中文字符(舒适区间)
*/

示意:

txt 复制代码
┌─────────────────────────────── 视口 ───────────────────────────────┐
|       ┌────────────────── 文章容器(max-width 680px) ────────────┐|
|       | 文本文本文本文本文本文本文本文本文本...               ||
|       └───────────────────────────────────────────────────────┘|
└─────────────────────────────────────────────────────────────────┘

这样在大屏上阅读体验会舒适很多。

13.4 REM + vw 解决方案比较

REM 和 vw 都可以做"随屏幕变化",但各有优缺点。

13.4.1 REM 方案

特点:

css 复制代码
/* REM 方案特点 */
/* 优点:
   - 基于根字号,所有rem单位都会等比缩放
   - 可以精确控制缩放比例
   - 适合整体布局和排版
   
   缺点:
   - 需要计算rem值
   - 某些元素可能不想缩放(如边框)
*/

/* 实际使用 */
.component {
  font-size: 1.25rem;    /* 20px on desktop, 17.5px on mobile */
  padding: 1rem 2rem;    /* 随根字号缩放 */
  border: 1px solid #ccc; /* px不缩放,保持细线 */
}
13.4.2 vw 方案
css 复制代码
/* VW 方案特点 */
/* vw = viewport width(视口宽度) */
/* 1vw = 视口宽度的1% */

/* 优点:
   - 直接随视口变化,无需媒体查询
   - 适合做全屏布局
   
   缺点:
   - 极端屏幕下可能过小/过大
   - 缺乏精细控制
*/

/* 例子:响应式标题 */
.hero-title {
  font-size: 5vw;        /* 视口宽1000px = 50px
                           视口宽400px = 20px
                           问题:在超宽屏可能过大 */
}

/* 更好的方案:结合clamp */
.hero-title {
  font-size: clamp(20px, 5vw, 60px);  /* 有上下界限的vw */
}
13.4.3 综合建议
  • 字体、组件尺寸:以 rem 为主,配合少量 clamp() + vw 做高级自适应
  • 布局宽度:% + max-width + rem 组合
  • 小装饰性的尺寸变化:可以适度用 vw 做效果

先用简单的 rem + 媒体查询写好一个稳定的响应式,再考虑引入 clamp/vw 这类"锦上添花"的方案。


第13章让你的组件"学会变形",从桌面到移动端都有良好体验。接下来第14章,我们会把色彩、间距、字号等抽象成 CSS 变量与计算,进一步提升维护性与可扩展性。

第14章 CSS 变量与计算

这一章解决的核心问题:

  • 怎样避免在 CSS 里到处复制粘贴同一套颜色、间距、阴影?
  • var():root 如何配合,定义一套主题?
  • 如何用 calc() 做简单的"数学运算",比如宽度减去边栏、间距组合?

14.1 var() 与 :root

CSS 变量(Custom Properties)允许你在 CSS 中定义和复用值。

14.1.1 定义与使用

定义变量:

css 复制代码
/* :root 伪类:代表文档的根元素(html),在这里定义全局变量 */
:root {
  /* 颜色变量:-- 开头表示自定义属性 */
  --color-primary: #2563eb;      /* 主色:蓝色 */
  --color-primary-soft: #dbeafe;  /* 浅主色:淡蓝色 */
  --color-bg: #0f172a;            /* 背景色:深色 */

  /* 圆角变量 */
  --radius-md: 12px;              /* 中等圆角 */
  --radius-lg: 20px;              /* 大圆角 */

  /* 间距变量 */
  --space-sm: 8px;                /* 小间距 */
  --space-md: 16px;               /* 中间距 */
  --space-lg: 24px;               /* 大间距 */
}

使用变量:

css 复制代码
/* var() 函数:使用CSS变量 */
.btn-primary {
  /* var(--变量名) 引用变量值 */
  background: var(--color-primary);     /* 使用主色 #2563eb */
  border-radius: var(--radius-md);      /* 使用中等圆角 12px */
  padding: 0.5rem var(--space-lg);      /* 右侧使用大间距 24px */
}

.section {
  background: var(--color-bg);          /* 使用背景色 #0f172a */
  padding: var(--space-lg);             /* 使用大间距 24px */
}

/* 优势:改变:root中的值,所有引用处都会更新 */

/* var() 还支持回退值(fallback):变量未定义时使用备用值 */
/* 语法:var(--变量名, 回退值) */
/* 示例:var(--color-primary, #2563eb) */
/* 当 --color-primary 未定义时,自动使用 #2563eb */
14.1.2 与预处理器变量的区别

如果你用过 Sass/LESS 之类的预处理器,里面也有"变量":

  • Sass 变量是在"编译阶段"展开的,浏览器看不到
  • CSS 变量在"运行时"存在,可以根据状态/主题动态改变

示例:

css 复制代码
/* 主题切换:在不同类名下重新定义变量 */

/* 亮色主题 */
.theme-light {
  --color-bg: #ffffff;     /* 白色背景 */
  --color-text: #0f172a;   /* 深色文字 */
}

/* 暗色主题 */
.theme-dark {
  --color-bg: #020617;     /* 深色背景 */
  --color-text: #e5e7eb;   /* 浅色文字 */
}

/* 组件使用变量,不关心具体主题 */
body {
  background: var(--color-bg);   /* 根据主题自动切换 */
  color: var(--color-text);      /* 根据主题自动切换 */
  
  /* 如果body有.theme-dark类,使用暗色值
     如果body有.theme-light类,使用亮色值 */
}

在 JS 中切换 body 上的 theme-light / theme-dark 类,即可完成主题切换,无需重新编译 CSS。

14.2 自定义主题

利用 CSS 变量可以很方便地做主题系统:

14.2.1 基础主题结构
css 复制代码
/* 完整的主题变量系统 */
:root {
  /* 默认亮色主题 */
  --color-bg: #f9fafb;                    /* 页面背景:浅灰 */
  --color-surface: #ffffff;               /* 卡片背景:纯白 */
  --color-primary: #2563eb;               /* 主色:明亮蓝 */
  --color-primary-soft: #dbeafe;          /* 浅主色:淡蓝 */
  --color-accent: #f97316;                /* 强调色:橙色 */
  --color-text: #0f172a;                  /* 文字颜色:深色 */
}

/* 暗色主题覆盖 */
.theme-dark {
  --color-bg: #020617;                    /* 页面背景:深蓝黑 */
  --color-surface: rgba(15, 23, 42, 0.9); /* 卡片背景:半透明深色 */
  --color-primary: #60a5fa;               /* 主色:柔和蓝 */
  --color-primary-soft: rgba(37, 99, 235, 0.25); /* 浅主色:透明蓝 */
  --color-accent: #fb923c;                /* 强调色:柔和橙 */
  --color-text: #e5e7eb;                  /* 文字颜色:浅色 */
}

然后组件层统一引用这些变量:

css 复制代码
/* 组件使用变量,自动适应主题 */
body {
  background: var(--color-bg);      /* 使用主题背景色 */
  color: var(--color-text);         /* 使用主题文字色 */
  transition: background 0.3s ease,  /* 主题切换时平滑过渡 */
              color 0.3s ease;
}

.card {
  background: var(--color-surface);  /* 使用表面色(卡片背景) */
  /* 亮色主题时是白色,暗色主题时是半透明深色 */
}

.btn-primary {
  background: var(--color-primary);  /* 使用主色 */
  /* 不同主题下有不同的蓝色调 */
}

只要在 htmlbody 上切换 .theme-dark 类,整站视觉就会切换到暗色主题。

14.2.2 局部主题

你也可以在某个局部容器上覆盖变量:

css 复制代码
/* 局部主题:在某个区域覆盖变量 */
.pricing-section {
  /* 在这个区域内重定义变量 */
  --color-surface: #0f172a;    /* 局部:深色表面 */
  --color-text: #e5e7eb;       /* 局部:浅色文字 */
}

.pricing-section .card {
  /* 这里的卡片会使用局部变量 */
  background: var(--color-surface);  /* 使用局部深色背景 */
  color: var(--color-text);         /* 使用局部浅色文字 */
  
  /* 优先级:局部变量 > 全局变量 */
}

这相当于给某一块区域「换皮肤」,而不影响其它页面部分。

14.3 calc() 的数学能力

calc() 允许你在 CSS 中做简单的计算:加、减、乘、除。

14.3.1 基础用法
css 复制代码
/* calc() 函数:在CSS中进行数学计算 */

/* 基础用法:固定值计算 */
.sidebar {
  width: 260px;                 /* 侧边栏固定宽度 */
}

.main {
  /* calc(): 计算100%减去260px */
  width: calc(100% - 260px);    /* 主内容区 = 总宽 - 侧边栏 */
}

结合变量(推荐):

css 复制代码
:root {
  --sidebar-width: 260px;       /* 定义侧边栏宽度变量 */
}

.sidebar {
  width: var(--sidebar-width);  /* 使用变量 */
}

.main {
  /* calc() + var(): 变量参与计算 */
  width: calc(100% - var(--sidebar-width));
  /*         总宽度 - 侧边栏宽度变量 */
  
  /* 优势:改变--sidebar-width,两处同时更新 */
}
14.3.2 间距与尺寸的组合

例如:卡片宽度减去左右 padding:

css 复制代码
/* 复杂计算示例:让子元素突破父元素padding */
.card {
  padding: var(--space-md);     /* 卡片有内边距 16px */
}

.card__media {
  /* 宽度计算:100% + 左右padding */
  width: calc(100% + var(--space-md) * 2);
  /*         100% + 16px × 2 = 100% + 32px */
  
  /* 负外边距:抵消父元素的padding */
  margin: calc(var(--space-md) * -1);
  /*           16px × -1 = -16px */
  /* ⚠️ 注意:不能写 margin: -var(--space-md),CSS 变量不支持直接加负号 */
  /*          必须用 calc() 来实现负值计算 */
  
  /* 效果:图片铺满卡片边缘,文字保持内边距 */
}

/* calc() 支持的运算:
   + 加法:calc(100% + 20px)
   - 减法:calc(100% - 20px)
   * 乘法:calc(2rem * 1.5)
   / 除法:calc(100% / 3)
   
   注意:
   - + 和 - 两侧必须有空格,否则会被解析为正负号而非运算符
     ✅ calc(100% - 20px)   ❌ calc(100%-20px)
   - * 和 / 不强制要求空格,但建议加上保持一致
*/

这种写法可以让某个子元素"铺满"卡片边缘,而文字仍然保持内边距。

示意:

txt 复制代码
┌──────────── card ────────────┐
│ [   card__media 铺满边缘   ] │ ← 用 margin 负值抵消 padding
│  文本文本文本......             │
└──────────────────────────────┘
14.3.3 与 clamp() 等函数组合

calc() 可以与 clamp() 等函数一起使用,构建更复杂的响应式表达式,但在入门阶段不必刻意追求"炫技"。

14.4 暗色模式与亮色模式切换

最后,我们把 CSS 变量、主题与媒体查询结合起来,实现暗色 / 亮色自动或手动切换。

14.4.1 跟随系统主题

可以使用媒体查询:

css 复制代码
/* 跟随系统主题:检测用户系统设置 */
/* prefers-color-scheme: 用户偏好的颜色方案 */
@media (prefers-color-scheme: dark) {
  /* 当用户系统设置为暗色模式时 */
  :root {
    --color-bg: #020617;                    /* 自动切换为暗色背景 */
    --color-surface: rgba(15, 23, 42, 0.9); /* 自动切换为暗色表面 */
    --color-text: #e5e7eb;                  /* 自动切换为浅色文字 */
  }
}

/* 也可以检测亮色模式(通常不需要单独写,:root 里的默认样式就是亮色) */
/* 只有在需要明确覆盖某些亮色专属样式时才用 */
@media (prefers-color-scheme: light) {
  /* 亮色模式的设置(大多数情况下这里留空,默认样式即亮色) */
}

这样在用户系统设置为"暗色模式"时,会自动应用暗色主题变量。

14.4.2 手动切换按钮

再配合一个简单的切换按钮(由 JS 控制类名):

css 复制代码
/* 手动切换主题:通过类名控制 */
.theme-dark {
  --color-bg: #020617;
  --color-surface: rgba(15, 23, 42, 0.9);
  --color-text: #e5e7eb;
}

body {
  background: var(--color-bg);
  color: var(--color-text);
  
  /* 平滑过渡,主题切换不突兀 */
  transition: background 0.3s ease,
              color 0.3s ease;
}

HTML:

html 复制代码
<!-- 主题切换按钮 -->
<button id="theme-toggle" class="btn btn--outline">
  <span class="theme-icon">🌙</span> 切换主题
</button>

JS(伪代码,仅作示意):

js 复制代码
// 点击按钮切换主题
document.getElementById('theme-toggle').addEventListener('click', () => {
  // toggle: 如果有这个类就移除,没有就添加
  document.body.classList.toggle('theme-dark');
  
  // 可选:保存到localStorage,下次访问记住用户选择
  const isDark = document.body.classList.contains('theme-dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

这里不展开 JS 细节,只要理解:利用 CSS 变量 + 类名切换,就能构建出灵活的主题系统。


第14章为你展示了如何把颜色、间距、字号等抽象成 CSS 变量,并通过 calc 等函数做简单计算,从而让样式的"维护"和"变更"都更加集中、高效。到这里,第五篇已经完成了从组件化、响应式到维护性的闭环,为后续的 CSS 架构与工程化打下了基础。

相关推荐
魔士于安6 小时前
Unity完整小球迷宫项目
前端·unity·游戏引擎·贴图·模型
irpywp6 小时前
苦于AI生成的网页千篇一律且粗糙?design-md-chrome :一款网页样式提取插件 ,将任意网站的视觉规范转化为大模型可读的代码指令!
前端·人工智能·chrome·开源·github
xingpanvip6 小时前
星盘接口开发文档:日运语料接口指南
android·开发语言·前端·css·php·lua
网络点点滴6 小时前
Node.js理论-Web的基本运作原理
前端·node.js
宝宝宝阿7 小时前
前端访问后台接口存在跨域问题,如何处理解决
前端
广州华水科技7 小时前
北斗GNSS与单北斗变形监测在水库安全监测中的应用探索
前端
蜡台7 小时前
使用 html javascript 实现 金币落袋效果
前端·javascript·html
IT_陈寒7 小时前
为什么我的Python multiprocessing总是卡在join()?
前端·人工智能·后端
李白的天不白7 小时前
VUE依赖配置问题
前端·javascript·vue.js