freeCodeCamp 响应式网页设计实战:用 CSS @keyframes 改变按钮的 Hover 状态
本篇围绕 freeCodeCamp 课程仓库中的挑战文件 use-css-animation-to-change-the-hover-state-of-a-button("Use CSS Animation to Change the Hover State of a Button")展开。你将学会如何用 @keyframes 规则配合 :hover 伪类,让按钮在悬停时平滑地改变背景色,并完整掌握该课程挑战的文件结构(seed 代码、测试断言、参考解答)与在 Responsive Web Design 认证中的位置,以及它如何衔接下一节 animation-fill-mode 的内容。
挑战目标:让按钮悬停时背景色变为 #4791d0
该挑战属于 freeCodeCamp 的 Responsive Web Design(响应式网页设计)认证,位于 applied-visual-design 模块中。从该模块的结构文件看,它在整个挑战顺序中紧随前置课 Learn How the CSS @keyframes and animation Properties Work(id 587d78a7367417b2b2512adf)之后,而模块又作为 responsive-web-design 超级块的第三个 block(在 basic-html-and-html5、basic-css 之后)出现,因此本课的动画知识建立在前面 CSS 基础之上。
挑战的任务描述(instructions)非常明确:
使用 CSS
@keyframes改变button元素的background-color,使它在用户悬停(hover)时变为#4791d0。@keyframes规则中只需要一个100%的条目。
同时文档特别提示:ms 是毫秒(milliseconds)的缩写,1000ms 等于 1s。本挑战中 animation-duration: 500ms 即表示动画持续 0.5 秒。
先理解原理:@keyframes 与动画属性的工作方式
官方给出的演示思路是:可以用 CSS @keyframes 在悬停状态中改变按钮的颜色。文档先举了一个"鼠标悬停时改变图片宽度"的例子:
<style>
img {
width: 30px;
}
img:hover {
animation-name: width;
animation-duration: 500ms;
}
@keyframes width {
100% {
width: 40px;
}
}
</style>
<img src="https://cdn.freecodecamp.org/curriculum/applied-visual-design/google-logo.png" alt="Google's Logo" />
这段代码包含三个关键部分,也是本挑战的核心模式:
:hover伪类 +animation-name:把动画"绑定"到元素的悬停状态。animation-name的值(这里是width)就是后面@keyframes规则声明的动画名称,两者靠名字匹配。animation-duration:设定动画的持续时间,500ms即 0.5 秒。@keyframes width:定义动画过程中"发生什么"。这里只写了100%一个帧,表示动画结束时的状态是width: 40px;起始状态(0%)则沿用元素原有的width: 30px,浏览器会在 500ms 内自动插值过渡。
前序课程 587d78a7367417b2b2512adf 对这一机制有更完整的讲解:animation-name 设定动画名称供 @keyframes 关联,animation-duration 设定动画时长,而 @keyframes 通过 0% 到 100% 的百分比"帧"指定每一阶段元素的样式——可以类比为电影,0% 是开场画面,100% 是片尾画面,CSS 负责在中间"演绎"过渡。
初始代码(seed)与需要补充的部分
挑战文件中的 --seed-contents-- 给出了编辑器的初始代码,它已经完成了 button 的基础样式和悬停动画的声明,唯独缺少 @keyframes 规则:
<style>
button {
border-radius: 5px;
color: white;
background-color: #0F5897;
padding: 5px 10px 8px 10px;
}
button:hover {
animation-name: background-color;
animation-duration: 500ms;
}
</style>
<button>Register</button>
其中 padding: 5px 10px 8px 10px 是四值简写,分别对应上、右、下、左的填充;按钮初始背景色为 #0F5897。button:hover 里已经声明了 animation-name: background-color 与 animation-duration: 500ms,但此时 background-color 这个动画名还没有对应的 @keyframes 定义——这正是学习者要补上的空白。
参考解答:补全 @keyframes 规则
文件 --solutions-- 部分给出的完整参考解答如下,在 button:hover 规则之后新增了一条 @keyframes background-color 规则:
<style>
button {
border-radius: 5px;
color: white;
background-color: #0F5897;
padding: 5px 10px 8px 10px;
}
button:hover {
animation-name: background-color;
animation-duration: 500ms;
}
@keyframes background-color {
100% {
background-color: #4791d0;
}
}
</style>
<button>Register</button>
执行逻辑是:当鼠标悬停在按钮上时,button:hover 规则生效,触发名为 background-color 的动画;由于 @keyframes 只定义了 100% 帧(background-color: #4791d0),动画从按钮当前的 #0F5897 出发,在 500ms 内平滑过渡到 #4791d0。
测试如何判定通过:两条正则断言
该挑战的验证逻辑写在 --hints-- 部分(hints 中的代码块即实际的 testString)。从源码看,测试采用正则表达式对编辑区代码做模式匹配,共有两条断言:
// 断言 1:必须存在名为 background-color 的 @keyframes 规则
assert.match(code, /@keyframes\s+?background-color\s*?{/g);
// 断言 2:该规则中必须有 100% 帧,且把 background-color 设为 #4791d0
assert.match(code, /100%\s*?{\s*?background-color:\s*?#4791d0;\s*?}/gi);
第一条正则要求代码中出现 @keyframes background-color {(空白字符允许弹性匹配);第二条要求在 100% 帧内写出 background-color: #4791d0;,且末尾分号与花括号结构完整。这解释了为什么任务强调"只写一个 100% 条目"——多写 0% 帧不会破坏断言,但会偏离课程意图(起始状态应由元素原有样式承担)。
从课程架构角度看,这种"挑战 = Markdown 分节 + 正则/JS 断言"的组织方式是统一的:challenge-schema.js 中的 Joi 模式要求每个挑战必须具备 tests 数组(含 text 与 testString)以及 solutions 等字段,seed、hints、solutions 都是被该模式校验后入库的结构化内容。
常见陷阱:动画结束后颜色回弹
这个写法有一个值得注意的行为:动画只定义了 500ms 的过程,鼠标保持悬停时动画播放完毕后,按钮会回落到原始背景色 #0F5897,而不是停留在 #4791d0。这是本挑战刻意保留的"未完成态"——它由下一课专门解决。
紧随其后的挑战 Modify Fill Mode of an Animation 指出:"Notice how the animation resets after 500ms has passed, causing the button to revert back to the original color. You want the button to stay highlighted."(注意动画在 500ms 后复位,按钮回到原色。你希望按钮保持高亮。)其解法是在 button:hover 中追加:
animation-fill-mode: forwards;
animation-fill-mode 控制动画结束后应用到元素上的样式;取值为 forwards 时,元素会保持 100% 帧的样式(即停留在 #4791d0),直到鼠标移开。至此,"悬停变色"的交互才算完整。
小结:这条学习链路的脉络
| 环节 | 内容 | 关键文件 |
|---|---|---|
| 前置原理 | animation-name / animation-duration / @keyframes 百分比帧的工作方式 |
587d78a7367417b2b2512adf.md |
| 本课任务 | 在 :hover 状态下用 @keyframes 把按钮背景色动画到 #4791d0,@keyframes 只含 100% 帧 |
587d78a7367417b2b2512ae0.md |
| 后续衔接 | 用 animation-fill-mode: forwards 让按钮悬停时保持高亮色 |
58a7a6ebf9a6318348e2d5aa.md |
| 模块/认证位置 | applied-visual-design 是 responsive-web-design 认证的第 3 个 block | applied-visual-design.json、responsive-web-design.json |
掌握本课的关键收获可以概括为三点:一是 :hover 伪类可以承载 animation-* 属性,把动画触发与交互状态绑定;二是 @keyframes 只写终点帧(100%)时,起点自动取元素当前值,适合做"从 A 变到 B"的两端动画;三是 animation-duration 支持 ms/s 两种时间单位(1000ms = 1s)。按挑战文件中的 seed 与 solutions 对照练习后,再进入 fill-mode 一课,即可完整实现一个"悬停后保持高亮"的按钮动画效果。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00