freeCodeCamp 响应式 Web 设计课程实战:用 CSS box-shadow 为卡片元素添加多层阴影
本篇以 freeCodeCamp 官方课程中的一个实战练习题为主体,讲解 CSS box-shadow 属性的完整参数体系(offset-x、offset-y、blur-radius、spread-radius、color)、多阴影叠加的写法与取值含义,并展示该练习题在课程代码库中的原始定义、测试断言与参考实现。读完后,你既能掌握"为卡片元素添加双层半透明阴影"这一常见视觉设计的落地写法,也能了解 freeCodeCamp 课程文件从 Markdown 到可运行题目的完整解析流程。
课程定位:这道题在 freeCodeCamp 课程中的位置
该练习题的源文件是 587d781b367417b2b2512abe.md,从文件的 YAML frontmatter 可以看到其元数据:
id: 587d781b367417b2b2512abe
title: Add a box-shadow to a Card-like Element
challengeType: 0
forumTopicId: 301031
dashedName: add-a-box-shadow-to-a-card-like-element
它在课程体系中的挂载关系可以从两处仓库配置中确认:
- 区块定义 applied-visual-design.json 中,该题以
challengeOrder数组第 11 个条目(id 为587d781b367417b2b2512abe)排在 "Adjust the Size of a Heading Element..." 之后、"Decrease the Opacity of an Element" 之前; - 超级区块定义 responsive-web-design.json 中,
applied-visual-design是 Responsive Web Design 课程的第 3 个 block(位于basic-html-and-html5、basic-css之后,applied-accessibility之前)。
也就是说,这道题属于"Applied Visual Design"(视觉设计应用)教学块:学员已经掌握了 text-align、width/height、background-color、font-size 等基础视觉属性之后,开始学习用 box-shadow 等属性塑造元素的立体感。challengeType: 0 对应仓库中 challenge-types.ts 里的 html 类型(取值 0),该类型在 viewTypes 中映射为 classic 经典双栏编辑器视图,在 submitTypes 中映射为 tests——即提交时运行题目自带的测试断言来判定是否通过,这正是本文后文分析测试正则的依据。
核心知识点:box-shadow 的五个参数
题目正文(# --description-- 小节)给出了 box-shadow 属性的权威描述:该属性可以为一个元素应用一个或多个阴影,其取值按以下固定顺序给出:
| 参数 | 是否必需 | 含义 |
|---|---|---|
offset-x |
必需 | 阴影相对元素在水平方向上偏移的距离 |
offset-y |
必需 | 阴影相对元素在垂直方向上偏移的距离 |
blur-radius |
可选 | 阴影的模糊半径,值越大阴影越柔和 |
spread-radius |
可选 | 阴影的扩散半径,正值让阴影比元素更大 |
color |
可选 | 阴影颜色 |
两个要点值得强调:
- 参数顺序不可调换。
offset-x和offset-y必须是前两个值,blur-radius、spread-radius、color依次排在其后,浏览器按位置解析而非按名称解析。 - 多阴影用逗号分隔。题目原文指出:"Multiple box-shadows can be created by using commas to separate properties of each box-shadow element." 每写一组完整的五元组、再跟一个逗号,就能叠加一层新阴影。
题目给出的标准示例正是"多层阴影 + 模糊 + 半透明黑色"的组合:
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
拆解这行 CSS 的两层阴影:
- 第一层
0 10px 20px rgba(0,0,0,0.19):水平偏移 0、垂直下移 10px、模糊半径 20px、未指定 spread、颜色为透明度 0.19 的黑色。它构成元素下方一大片"远景"柔光阴影; - 第二层
0 6px 6px rgba(0,0,0,0.23):垂直下移 6px、模糊半径 6px、透明度 0.23。它更贴近元素本体,相当于"近景"接触阴影。
两层叠加后,卡片会呈现出有层次、有深度的悬浮效果:远处一层大而淡的模糊,近处一层小而略深的阴影。这里没有使用 spread-radius(第四位),题目特意提示它是可选参数——省略时浏览器按 0 处理。另外注意 rgba(0,0,0,0.19) 这类低透明度黑色是 Material Design 风格阴影的常用做法:用黑色加透明度代替纯 black,让阴影能与任何背景色自然融合。
题目要求与卡片种子代码
# --instructions-- 小节给出的任务说明是:元素的 id 现在改为了 thumbnail,请用这个 id 选择器,套用上面的示例 CSS 值,为卡片加上 box-shadow。
配套的种子代码(# --seed-- → ## --seed-contents--)是一段完整的 HTML+CSS 文档,模拟了一张"Alphabet/Google 创始人"信息卡片:
<style>
h4 {
text-align: center;
background-color: rgba(45, 45, 45, 0.1);
padding: 10px;
font-size: 27px;
}
p {
text-align: justify;
}
.links {
text-align: left;
color: black;
}
.fullCard {
width: 245px;
border: 1px solid #ccc;
border-radius: 5px;
margin: 10px 5px;
padding: 4px;
}
.cardContent {
padding: 10px;
}
.cardText {
margin-bottom: 30px;
}
</style>
<div class="fullCard" id="thumbnail">
<div class="cardContent">
<div class="cardText">
<h4>Alphabet</h4>
<hr>
<p><em>Google was founded by Larry Page and Sergey Brin while they were <u>Ph.D. students</u> at <strong>Stanford University</strong>.</em></p>
</div>
<div class="cardLinks">
<a href="https://en.wikipedia.org/wiki/Larry_Page" target="_blank" class="links">Larry Page</a><br><br>
<a href="https://en.wikipedia.org/wiki/Sergey_Brin" target="_blank" class="links">Sergey Brin</a>
</div>
</div>
</div>
这段种子代码的结构值得注意:
- 外层
div.fullCard同时带有id="thumbnail"——id 选择器#thumbnail与 class 选择器.fullCard指向同一个元素,卡片已有的border(1px 实线 #ccc)、border-radius(5px 圆角)、width: 245px等样式构成了阴影作用的基础外形; - 内部的
h4标题、hr分隔线、说明段落和两条 Wikipedia 外链,正是此前若干道练习题(text-align、strong、u、em、hr等)逐步搭建起来的内容,本题在其之上继续叠加视觉效果——这体现了该教学块"在同一张卡片上逐题累加样式"的课程设计思路。
标准答案:用 id 选择器落位阴影
题目 # --solutions-- 小节给出的参考答案,与种子代码的唯一差异就是新增了一个 #thumbnail 规则块:
<style>
h4 {
text-align: center;
background-color: rgba(45, 45, 45, 0.1);
padding: 10px;
font-size: 27px;
}
p {
text-align: justify;
}
.links {
text-align: left;
color: black;
}
#thumbnail {
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);
}
.fullCard {
width: 245px;
border: 1px solid #ccc;
border-radius: 5px;
margin: 10px 5px;
padding: 4px;
}
.cardContent {
padding: 10px;
}
.cardText {
margin-bottom: 30px;
}
</style>
<!-- 其余 div 结构与种子代码完全相同 -->
要点:答案把阴影写在了 id 选择器 #thumbnail 下,而不是 class 选择器 .fullCard 下——题目指令明确要求"With this selector"(用这个 id 选择器)。id 选择器优先级高于 class 选择器,后续其他题目若再调整 .fullCard 的样式,也不影响这条阴影声明;这也是在既有样式表上增量开发时的一种稳妥习惯。
测试断言分析:题目如何自动判定你的答案
# --hints-- 小节中的两段测试代码是判定通过与否的真正依据,它们直接对学员提交的全部代码做正则匹配:
第一条:必须存在 #thumbnail { 规则块,且其中出现 box-shadow 属性。
assert.match(code,(/#thumbnail\s*?{\s*?box-shadow/g));
第二条:box-shadow 的取值必须与标准值逐段一致——两层阴影的偏移、模糊半径和 rgba 颜色都要匹配(正则中的 \s*? 允许数字与单位、参数之间存在任意空白,gi 标志使其忽略大小写且全局匹配)。
assert(
code.match(
/box-shadow:\s*?0\s+?10px\s+?20px\s+?rgba\(\s*?0\s*?,\s*?0\s*?,\s*?0\s*?,\s*?0?\.19\)\s*?,\s*?0\s+?6px\s+?6px\s+?rgba\(\s*?0\s*?,\s*?0\s*?,\s*?0\s*?,\s*?0?\.23\)/gi
)
);
从这两条断言可以读出几层设计意图:
- 第一条用
g(全局)标志,意味着即使样式表里有多条box-shadow声明,只要任意一处满足"#thumbnail块内有box-shadow"即可命中,但属性必须确实出现在 id 选择器内部——写在.fullCard下不会通过; - 第二条没有强制"必须只有这一条 box-shadow",但要求取值顺序严格为
0 10px 20px rgba(0,0,0,0.19)后接0 6px 6px rgba(0,0,0,0.23),顺序写反(例如把 0.23 那层放前面)会匹配失败——这正是前文强调"参数顺序固定"在教学判定上的直接体现; - 正则对空白高度宽容(
\s*?/\s+?),学员在rgba( 0 , 0 , 0 , 0.19 )中多敲空格也能通过,降低了判定的脆弱性。
由于该题 challengeType: 0(html),按 challenge-types.ts 中 submitTypes 的定义,提交动作会走 tests 流程执行上述断言;viewTypes 则决定前端以 classic 经典编辑器视图渲染题目。
题目定义如何变成可运行题目:仓库解析管线
这道题的 Markdown 文件并非直接被网站消费,而是经过 freeCodeCamp 的 curriculum 工具链解析。以本文件为例,解析器入口 tools/challenge-parser/parser 使用若干插件按 # --xxx-- 小节切分内容:
- 种子代码提取插件 add-seed.js 负责读取
# --seed--小节,并在其内部定位## --seed-contents--——源码中有一行强制校验:## --seed-contents-- must appear in # --seed-- sections,即种子内容小节必须位于 seed 小节之内,与本文件的组织结构一致。该插件还会把代码块内容整理为challengeFiles写进题目数据,并顺带剥离--fcc-editable-region--标记(本卡片的# --seed--小节中,种子 HTML 前的空行即为该标记被移除后的位置); - 测试与答案插件 add-tests.js、add-solution.js 则分别消费
# --hints--和# --solutions--小节; - 最终产出的题目对象由 challenge-schema.js 中的 Joi 模式校验,其中
challengeType被约束为 0–33 的整数(Joi.number().min(0).max(33)),helpCategory必须取自JavaScript、HTML-CSS、Python等枚举之一——本块在区块配置中声明的helpCategory恰为HTML-CSS。
因此,本文前面逐段分析的 --description--、--instructions--、--hints--、--seed--、--solutions-- 五个小节,实际上分别对应题目的讲解文本、操作指令、自动判定断言、初始代码与参考解答——这正是 freeCodeCamp 把"一篇教学文档"直接编译为"一道可交互、可判定的练习题"的完整管线。
小结与延伸
回顾本篇从这道课程题中可以带走的核心内容:
box-shadow的取值按offset-x、offset-y、blur-radius、spread-radius、color的固定顺序解析,后三项可选,多阴影以逗号分隔叠加;- "近处小而深 + 远处大而淡"的双层
rgba(0,0,0,低透明度)阴影,是卡片类元素制造悬浮立体感的经典配方:box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);; - 增量修改既有卡片样式时,优先使用题目指定的 id 选择器(
#thumbnail)落位新属性,避免与 class 样式耦合; - 从源码结构看,freeCodeCamp 的每道课程题都是一个带 YAML frontmatter 的五段式 Markdown 文件,经
challenge-parser插件族解析、Joi 模式校验后进入课程体系,测试正则即判定标准。
若要继续沿着本教学块学习,可参考同区块内紧随其后的题目文件(如透明度、text-transform 等,见 applied-visual-design 目录),以及块级定义 applied-visual-design.json 中完整的 50 道题顺序。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00