第五篇 构建真实页面:组件、响应式、维护性
目标:把前面学到的盒模型、布局、视觉、动效,真正落地到"真实项目"的代码里。
看完这一篇,你应该能:
- 用「组件化」的思维组织 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:一个相对独立的"模块/组件"(如
card、button、nav) - Element:组件内部的组成部分(如
card__title、card__content) - Modifier:在"基础样式"上做的变化(如
card--primary、card--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 的阶段,不需要追求完美的设计系统,但可以尽早养成两个习惯:
-
用组件的视角看页面:
- 看到一个页面时,先问:这里面有哪些"可复用组件"?
- 比如:卡片、列表项、按钮、标签、导航、页脚等
-
减少复制粘贴:
- 当你发现自己在多个地方写了类似结构的 HTML/CSS,就考虑抽成一个组件类
- 例如
card、btn这些基础组件
本章的目标,是让你从"给页面上色"升级到"给组件上色"。接下来第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 */
/* 桌面显示器、大屏笔记本 */
}
更重要的是:断点应该跟"设计发生明显变化"的地方对齐,而不是盲目抄别人的数值。
一个实用的做法是:
- 先在浏览器中缩放窗口
- 当你观察到某个组件「开始看起来不舒服」时
- 在那个宽度附近设置断点
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 常见布局模式的适配
举几个常见例子:
- 两栏布局(左图右文) :
- 桌面:左右分栏
- 手机:上下堆叠
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继续生效,变成上下间距 */
}
}
- 多列卡片网格 :
- 桌面: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= 根元素html的font-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); /* 使用主色 */
/* 不同主题下有不同的蓝色调 */
}
只要在 html 或 body 上切换 .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 架构与工程化打下了基础。