最近花了一些时间重新看了下 CSS 动画。
以前做页面的时候,transition、transform、animation 这些东西其实经常用,但基本都是遇到需求就写,知道怎么实现,却很少认真想过它们之间到底有什么关系。
这次想着把 CSS 特效重新整理一下,顺便多做了一些小实验。
从最基础的位移、缩放、旋转开始,一路玩到渐变、发光、模糊、Mask、Clip-path、3D、文字 Glitch、Aurora 背景,再到现在比较新的 Scroll-driven Animation 和 @property。
整理的过程中发现,很多看起来很复杂的效果,其实都是几个简单的 CSS 能力组合起来的。
所以这篇文章就当作一份自己的学习记录,把一些比较有意思的东西放在这里。
如果让我挑几个 CSS 里最值得玩的属性,我应该会先选 transform。
最常见的几个:
transform: translateX(100px);
transform: scale(1.2);
transform: rotate(45deg);
transform: skewX(20deg);
分别对应移动、缩放、旋转和倾斜。
比如一个卡片 hover:
.card {
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-6px) scale(1.02);
}
其实到这里已经能做出很多东西了。
以前我比较容易把这些属性当成一个个独立的 API 去记,后来发现更好理解的方式,是把它们看成几个“动作”:
translate:移动scale:变大变小rotate:旋转skew:倾斜然后再用 transition 或 animation 让这些动作动起来。
很多 CSS 特效基本都是在玩这些东西的组合。
还有一个比较容易忽略的东西:
transform-origin: center;
默认情况下,元素一般是围绕中心点进行变换。
但如果改成:
transform-origin: left center;
效果就完全不一样了。
比如做一个门打开的效果:
.door {
transform-origin: left center;
transition: transform 0.6s ease;
}
.door:hover {
transform: rotateY(-90deg);
}
这时候就开始有一点 3D 的感觉了。
transition 我以前用得非常多,但基本就是:
transition: all 0.3s;
后来认真看了一下,发现它其实就是在解决一个很简单的问题:
两个状态之间,不要突然跳过去,中间的过程帮我补出来。
例如:
.button {
transform: scale(1);
}
.button:hover {
transform: scale(1.05);
}
没有 transition 的时候,就是瞬间变化。
加上:
.button {
transition: transform 0.3s ease;
}
就变成:
scale(1)
↓
1.01
↓
1.02
↓
1.03
↓
1.04
↓
1.05
当然真实的变化并不一定是匀速的,这就涉及到下面的 timing function。
同样是从 A 到 B,有的动画感觉很生硬,有的却很自然。
很大一部分原因就是速度变化不一样。
最常见的:
linear
ease
ease-in
ease-out
ease-in-out
比如 linear:
速度
│
│ /
│ /
│ /
│/
└──────── 时间
就是匀速。
如果做一个无限旋转:
.loading {
animation: rotate 1s linear infinite;
}
这种时候匀速就比较合适。
但如果做一个弹窗出现:
.modal {
transition:
transform 0.3s ease-out,
opacity 0.3s ease-out;
}
ease-out 通常会更自然一些。
真正有意思的是:
cubic-bezier()
例如:
transition-timing-function:
cubic-bezier(0.68, -0.55, 0.265, 1.55);
它可以把动画的速度曲线控制得非常细。
有时候一个动画看起来很“高级”,并不是用了什么特别复杂的 CSS,而只是它的速度曲线调得比较舒服。
可以做出:
如果以后想专门研究动画,其实 cubic-bezier 很值得单独研究一下。
还有一个比较有意思的是:
steps(10)
它不是连续变化,而是把动画切成几个步骤。
比如做打字机效果:
.typing {
width: 20ch;
overflow: hidden;
white-space: nowrap;
animation: typing 2s steps(20);
}
@keyframes typing {
from {
width: 0;
}
to {
width: 20ch;
}
}
这种效果就很适合 steps()。
如果 transition 更像:
A → B
那 @keyframes 就可以自己定义一条完整的时间线。
最简单:
@keyframes move {
from {
transform: translateX(0);
}
to {
transform: translateX(100px);
}
}
然后:
.box {
animation: move 1s ease;
}
不过真正有意思的是中间可以插很多状态。
比如弹跳:
@keyframes bounce {
0% {
transform: translateY(0);
}
30% {
transform: translateY(-30px);
}
50% {
transform: translateY(0);
}
70% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
这样就可以自己控制整个动画过程。
一个元素不一定只能有一个动画。
比如:
.box {
animation:
rotate 3s linear infinite,
float 2s ease-in-out infinite,
glow 1.5s ease-in-out infinite alternate;
}
这样一个元素可以同时:
很多看起来挺复杂的 CSS 动画,拆开之后其实就是几个小动画叠在一起。
如果开始认真玩 CSS 特效,渐变基本绕不开。
常见的:
linear-gradient()
radial-gradient()
conic-gradient()
最简单:
background:
linear-gradient(
90deg,
#ff0080,
#7928ca
);
一个普通的渐变背景就出来了。
但它其实还能拿来做很多东西。
例如流光:
background:
linear-gradient(
90deg,
transparent,
white,
transparent
);
然后不断移动背景位置:
background-size: 200% 100%;
animation: shine 2s linear infinite;
就可以得到一道光扫过去的感觉。
径向渐变:
background:
radial-gradient(
circle,
#7c3aed,
transparent
);
特别适合做光晕。
如果再加上:
filter: blur(40px);
就很容易做出 Aurora、发光球之类的效果。
这个我以前用得相对少一些,但做特效非常好玩:
background:
conic-gradient(
red,
yellow,
blue,
red
);
可以拿来做:
如果再让它旋转:
animation: rotate 3s linear infinite;
效果就出来了。
CSS 的 filter 其实挺强。
常见的:
filter: blur(10px);
filter: brightness(1.2);
filter: contrast(1.5);
filter: grayscale(1);
filter: saturate(2);
filter: sepia(1);
filter: hue-rotate(90deg);
比如图片 hover:
.image {
filter: grayscale(1);
transition: filter 0.3s;
}
.image:hover {
filter: grayscale(0);
}
这种效果很简单,但实际项目里经常能用到。
blur() 在各种视觉效果里出现频率很高。
例如一个发光球:
.glow {
width: 200px;
height: 200px;
border-radius: 50%;
background: #7c3aed;
filter: blur(60px);
}
多个这样的球叠起来,再稍微移动一下,就能做出挺漂亮的背景。
普通阴影:
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.2);
多叠几层,就可以做发光。
例如:
text-shadow:
0 0 5px white,
0 0 10px #7c3aed,
0 0 20px #7c3aed,
0 0 40px #7c3aed;
这基本就是霓虹文字的核心思路。
这个平时写业务可能不太常用,但做视觉效果的时候挺有意思。
mix-blend-mode: screen;
可以让元素和下面的内容产生颜色混合。
做一些光效、图片叠加的时候可能会用到。
这两个东西我觉得是从“普通 CSS”走向“CSS 特效”的一个分界点。
比如裁剪成圆形:
clip-path: circle(50%);
三角形:
clip-path:
polygon(
50% 0,
100% 100%,
0 100%
);
甚至可以自己画一个六边形。
更有意思的是,它还能做动画。
比如:
.reveal {
clip-path: inset(0 100% 0 0);
transition: clip-path 0.8s ease;
}
.reveal:hover {
clip-path: inset(0 0 0 0);
}
就可以做一个从左往右出现的效果。
Mask 可以简单理解成:
给元素加一张“透明度地图”。
黑色、白色、透明度不同的区域,可以决定元素哪里显示,哪里隐藏。
例如:
mask-image:
linear-gradient(
to bottom,
black,
transparent
);
就可以做渐隐。
Mask 很适合做:
文字其实是 CSS 特效里非常好玩的一个方向。
.title {
background:
linear-gradient(
90deg,
#ff0080,
#7928ca
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.title {
color: white;
text-shadow:
0 0 5px white,
0 0 10px #7c3aed,
0 0 20px #7c3aed;
}
流光其实也没什么神秘的。
大概就是:
渐变
↓
把渐变放大
↓
移动 background-position
↓
让光从文字上扫过去
例如:
.title {
background:
linear-gradient(
90deg,
#fff 0%,
#7c3aed 50%,
#fff 100%
);
background-size: 200% 100%;
background-clip: text;
color: transparent;
animation: shine 3s linear infinite;
}
@keyframes shine {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
Glitch 效果看起来比较复杂,但拆开其实就是:
原文字
+
伪元素
+
复制文字
+
颜色错位
+
裁剪
+
动画
例如:
.glitch {
position: relative;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
inset: 0;
}
.glitch::before {
transform: translate(2px, -1px);
}
.glitch::after {
transform: translate(-2px, 1px);
}
再配合 clip-path 做不同区域的错位,就能慢慢接近真正的 Glitch 效果。
这种东西特别适合自己一点一点调参数。
网页背景其实是一个很大的 CSS 特效 playground。
一个很常见的网格背景:
background-image:
linear-gradient(
rgba(255, 255, 255, 0.08) 1px,
transparent 1px
),
linear-gradient(
90deg,
rgba(255, 255, 255, 0.08) 1px,
transparent 1px
);
background-size: 40px 40px;
本质上就是两组渐变交叉。
点阵也差不多:
background-image:
radial-gradient(
circle,
rgba(255, 255, 255, 0.2) 1px,
transparent 1px
);
background-size: 20px 20px;
Aurora 是我比较喜欢的一类背景效果。
它其实可以拆成:
多个圆形
↓
不同颜色
↓
radial-gradient
↓
blur
↓
opacity
↓
慢慢移动
例如:
.orb {
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
filter: blur(80px);
opacity: 0.6;
}
然后放几个不同颜色的球,让它们慢慢移动。
看起来挺复杂的背景,其实就是这些基础能力堆起来的。
CSS 特效最终还是要回到 UI。
按钮可以玩的东西非常多:
比如:
.button {
transition:
transform 0.2s,
box-shadow 0.2s;
}
.button:hover {
transform: translateY(-2px);
}
.button:active {
transform: translateY(1px) scale(0.98);
}
其实一个简单的 hover + active,就已经能让按钮的交互感提升很多。
卡片比较常见的是:
.card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15);
}
再往上加:
就可以做出完全不同的感觉。
一个简单的 Modal 出现动画:
.modal {
opacity: 0;
transform: scale(0.95) translateY(10px);
transition:
opacity 0.2s,
transform 0.2s;
}
.modal.open {
opacity: 1;
transform: scale(1) translateY(0);
}
这里其实还是之前说的:
状态 A
↓
opacity + transform
↓
transition
↓
状态 B
Skeleton 的经典思路也很简单:
灰色背景
+
渐变光带
+
background-position 移动
例如:
.skeleton {
background:
linear-gradient(
90deg,
#eee 25%,
#ddd 37%,
#eee 63%
);
background-size: 400% 100%;
animation:
skeleton 1.4s ease infinite;
}
CSS 变量平时写主题、颜色的时候用得比较多:
:root {
--primary: #7c3aed;
--radius: 12px;
}
但它其实也可以拿来做动画。
比如:
.box {
--x: 0px;
transform: translateX(var(--x));
}
然后通过 JavaScript 修改:
element.style.setProperty('--x', '100px');
这种方式很适合:
JavaScript
↓
提供数据
↓
CSS Variable
↓
CSS 负责视觉
这样 JS 不需要去控制每一帧怎么画,CSS 自己处理动画。
再往前一步就是 @property。
比如:
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
然后:
.box {
--angle: 0deg;
animation: rotate-angle 2s linear infinite;
}
@keyframes rotate-angle {
to {
--angle: 360deg;
}
}
@property 可以让自定义属性拥有明确的类型,也让一些以前不太容易做的 CSS 动画变得更好实现。
这个算是比较现代的 CSS 能力了。
这个最近也挺值得关注。
以前的动画基本是:
时间
↓
动画进度
Scroll-driven Animation 可以变成:
页面滚动
↓
滚动进度
↓
动画进度
也就是说:
滚动到哪里,动画就进行到哪里。
例如:
@keyframes reveal {
from {
opacity: 0;
transform: translateY(100px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: reveal linear both;
animation-timeline: view();
}
这种效果特别适合:
以前这种效果可能第一反应是 JavaScript 监听滚动,现在很多场景已经可以直接交给 CSS。
CSS 其实也能做一些挺有意思的 3D 效果。
常见的:
perspective
transform-style
translateZ
rotateX
rotateY
rotateZ
backface-visibility
.container {
perspective: 1000px;
}
可以先简单理解成:
我站在离这个 3D 世界多远的地方看它。
距离不同,3D 的感觉也不同。
.card {
transform:
rotateX(10deg)
rotateY(-15deg);
}
就可以让一个平面的卡片产生立体倾斜。
再结合鼠标位置动态改变角度,就可以做出很多现在网站上常见的 3D Card。
翻牌是最经典的 CSS 3D Demo 之一:
.card {
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card:hover {
transform: rotateY(180deg);
}
再给背面:
backface-visibility: hidden;
就能实现正反面翻转。
写 CSS 动画的时候,经常会看到:
动画尽量使用
transform和opacity。
刚开始的时候我也只是记住这句话,并没有特别理解为什么。
浏览器渲染页面,大致可以理解成:
CSS / JavaScript
↓
Style
↓
Layout
↓
Paint
↓
Composite
↓
屏幕
并不是每个 CSS 属性发生变化,浏览器做的工作都一样。
例如:
width
height
top
left
margin
padding
这些属性的变化可能会影响页面布局。
如果一个元素动一下,导致周围很多元素的位置都需要重新计算,成本自然会更高。
还有一些变化需要重新绘制。
比如:
特别是大面积的 blur、复杂 shadow 等效果,在大量元素同时使用时,也可能产生比较明显的性能成本。
而:
transform
opacity
在很多情况下更适合做高频动画。
比如:
transform: translateX(100px);
通常会比:
left: 100px;
更适合用来制作移动动画。
这里并不是说:
transform永远不会触发布局,也不是用了 transform 就一定流畅。
真正的渲染过程比这个复杂很多。
只是从动画设计的角度来说,transform 和 opacity 往往是比较好的选择。
还有一个经常被提到的:
will-change: transform;
它相当于提前告诉浏览器:
这个元素马上可能要发生变化。
但这个东西也不能乱用。
尤其不要看到性能问题就直接:
will-change: all;
大量使用反而可能带来额外的内存和合成层开销。
如果继续往 CSS 更底层走,就会碰到 Houdini。
它有一些比较有意思的能力:
@property 其实就和这套体系有关。
普通情况下,我们更多是在使用浏览器已经提供好的 CSS 能力。
而 Houdini 更像是:
浏览器的 CSS 能力
↓
开放一部分接口
↓
开发者可以参与
↓
自定义一些 CSS 行为
这个方向目前更偏高级和实验性质。
如果只是做普通业务,暂时不一定需要深入。
但如果喜欢研究浏览器渲染和视觉效果,还是挺有意思的。
这次整理 CSS 特效之后,我最大的一个感受是:
很多看起来很复杂的效果,拆开以后其实并没有那么复杂。
比如一个 Neon Button:
Gradient
+
Box Shadow
+
Text Shadow
+
Pseudo Element
+
Transform
+
Transition
+
Animation
Aurora:
多个圆形
+
radial-gradient
+
blur
+
opacity
+
transform
+
animation
Glitch:
文字
+
伪元素
+
颜色错位
+
clip-path
+
transform
+
animation
Skeleton:
Gradient
+
background-size
+
background-position
+
animation
看起来最后是一个很复杂的效果,但底层其实还是那些熟悉的 CSS 属性。
以后看到一个网页上的特效,我会尽量先不去搜索它的代码,而是先想一下:
这个效果到底发生了什么?
比如看到一个流光:
有一条亮光在移动
↓
应该是一个渐变
↓
渐变的位置在移动
↓
background-position
↓
animation
看到一个卡片浮起来:
卡片向上移动
↓
translateY
↓
阴影变大
↓
box-shadow
↓
hover 时发生
↓
transition
这样慢慢拆下来,会发现 CSS 其实挺有意思。
以前遇到一个效果,第一反应是“这个怎么写”。
现在会更倾向于先想:
这个效果是由哪些视觉变化组成的?
然后再去找对应的 CSS。
可能这才是这次重新整理 CSS 动画之后,觉得比较有收获的一点。
这篇更像是一次整理,而不是一份完整的 CSS 手册。
很多东西我也还在慢慢研究,尤其是 @property、Scroll-driven Animation、Houdini 这些相对新的能力,后面如果实际项目里用到,再继续记录一些具体的实验。
CSS 这种东西其实很适合边学边玩。
写一个小 Demo,改几个参数,看一下效果,再拆一下它到底是怎么实现的。
有时候折腾半天,最后发现只是几个渐变、一个伪元素和几行动画。
这种过程还挺有意思。
还没有公开评论
欢迎留下第一条想法,评论会在博主审核后显示。