css-native

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CSS Native — Zero-Dependency Animations & Visual Techniques

CSS原生方案——零依赖动画与视觉技术

When to Use CSS Native vs Library

何时选择CSS原生方案 vs 动画库

SituationDecision
< 3 animations on the pageCSS native
Scroll-driven reveal/parallaxCSS native (
animation-timeline
)
Enter/exit from
display: none
CSS native (
@starting-style
+
transition-behavior: allow-discrete
)
Tooltip/popover positioningCSS native (anchor positioning)
Page transitions (MPA or SPA)CSS native (View Transitions API)
Complex multi-step timeline (5+ tweens)GSAP
Stagger across dynamic list (unknown count)GSAP or Framer Motion
Physics-based spring with interruptionFramer Motion
Morph between SVG shapesGSAP MorphSVG
Rule of thumb: if you can express it in a
@keyframes
+ one
animation-timeline
, stay in CSS. The moment you need imperative control, sequence coordination, or runtime values — reach for a library.

场景决策
页面动画少于3个CSS原生方案
滚动驱动的显示/视差效果CSS原生方案(
animation-timeline
display: none
状态进入/退出
CSS原生方案(
@starting-style
+
transition-behavior: allow-discrete
提示框/弹出层定位CSS原生方案(锚点定位)
页面过渡(MPA或SPA)CSS原生方案(View Transitions API)
复杂多步骤时间线(5个以上补间动画)GSAP
动态列表的交错动画(数量未知)GSAP或Framer Motion
支持中断的物理弹簧动画Framer Motion
SVG形状间的变形动画GSAP MorphSVG
经验法则:如果可以用
@keyframes
+ 单个
animation-timeline
实现,就用CSS。当你需要命令式控制、序列协调或运行时数值时,再选择动画库。

Scroll-Driven Animations

滚动驱动动画

Scroll Progress Timeline

滚动进度时间线

Animate based on scroll position of a container.
css
.progress-bar {
  animation: grow-width linear both;
  animation-timeline: scroll(root block);
}

@keyframes grow-width {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
  • scroll(<scroller> <axis>)
    — scroller:
    nearest
    |
    root
    |
    self
    , axis:
    block
    |
    inline
    |
    x
    |
    y
  • Default:
    scroll(nearest block)
基于容器的滚动位置实现动画。
css
.progress-bar {
  animation: grow-width linear both;
  animation-timeline: scroll(root block);
}

@keyframes grow-width {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
  • scroll(<scroller> <axis>)
    — 滚动容器:
    nearest
    |
    root
    |
    self
    ,轴:
    block
    |
    inline
    |
    x
    |
    y
  • 默认值:
    scroll(nearest block)

View Progress Timeline

视图进度时间线

Animate as an element enters/exits the scrollport.
css
.reveal {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: translateY(0); }
}
元素进入/退出滚动视口时触发动画。
css
.reveal {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: translateY(0); }
}

animation-range

animation-range

Controls which segment of the timeline drives the animation.
css
/* Named ranges: cover | contain | entry | exit | entry-crossing | exit-crossing */
animation-range: entry 0% entry 100%;    /* animate during entry only */
animation-range: contain 0% contain 100%; /* animate while fully visible */
animation-range: entry 25% exit 75%;      /* custom start/end */
Use the Scroll-driven Animations tool to visualize ranges interactively.

控制时间线中驱动动画的片段。
css
/* 命名范围:cover | contain | entry | exit | entry-crossing | exit-crossing */
animation-range: entry 0% entry 100%;    /* 仅在进入阶段动画 */
animation-range: contain 0% contain 100%; /* 完全可见时动画 */
animation-range: entry 25% exit 75%;      /* 自定义开始/结束点 */
使用滚动驱动动画工具交互式可视化范围。

View Transitions API

View Transitions API

Same-Document (SPA)

同文档(SPA)

js
document.startViewTransition(() => {
  // Update the DOM synchronously
  updateContent();
});
css
/* Control the transition animation */
::view-transition-old(root) {
  animation: fade-out 200ms ease-out;
}
::view-transition-new(root) {
  animation: fade-in 300ms ease-in;
}

/* Named transitions for specific elements */
.hero-image { view-transition-name: hero; }

::view-transition-group(hero) {
  animation-duration: 400ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
js
document.startViewTransition(() => {
  // 同步更新DOM
  updateContent();
});
css
/* 控制过渡动画 */
::view-transition-old(root) {
  animation: fade-out 200ms ease-out;
}
::view-transition-new(root) {
  animation: fade-in 300ms ease-in;
}

/* 特定元素的命名过渡 */
.hero-image { view-transition-name: hero; }

::view-transition-group(hero) {
  animation-duration: 400ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

Cross-Document (MPA)

跨文档(MPA)

css
/* Both pages need this */
@view-transition { navigation: auto; }

/* Outgoing page */
.card { view-transition-name: card-detail; }

/* Incoming page */
.detail-hero { view-transition-name: card-detail; }
css
/* 两个页面都需要添加 */
@view-transition { navigation: auto; }

/* 离开页面 */
.card { view-transition-name: card-detail; }

/* 进入页面 */
.detail-hero { view-transition-name: card-detail; }

view-transition-class (group styling)

view-transition-class(组样式)

css
.card { view-transition-class: card; }

::view-transition-group(*.card) {
  animation-duration: 350ms;
  animation-timing-function: var(--ease-spring);
}

css
.card { view-transition-class: card; }

::view-transition-group(*.card) {
  animation-duration: 350ms;
  animation-timing-function: var(--ease-spring);
}

@starting-style

@starting-style

Native enter animations from
display: none
— no JS timing hacks.
css
.dialog {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 300ms ease, transform 300ms ease,
              display 300ms allow-discrete;

  @starting-style {
    opacity: 0;
    transform: translateY(-1rem);
  }
}

.dialog[hidden] {
  opacity: 0;
  transform: translateY(-1rem);
  display: none;
}
Key rules:
  • transition-behavior: allow-discrete
    (or shorthand
    allow-discrete
    in the transition) enables transitioning
    display
    and
    overlay
  • @starting-style
    block defines the "from" state when the element first renders
  • Combine with
    [popover]
    and
    <dialog>
    for native modals with zero JS animation code

原生支持从
display: none
状态进入的动画——无需JS计时技巧。
css
.dialog {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 300ms ease, transform 300ms ease,
              display 300ms allow-discrete;

  @starting-style {
    opacity: 0;
    transform: translateY(-1rem);
  }
}

.dialog[hidden] {
  opacity: 0;
  transform: translateY(-1rem);
  display: none;
}
关键规则:
  • transition-behavior: allow-discrete
    (或在transition中使用简写
    allow-discrete
    )启用
    display
    overlay
    属性的过渡
  • @starting-style
    块定义元素首次渲染时的“起始”状态
  • 结合
    [popover]
    <dialog>
    实现零JS动画代码的原生模态框

Anchor Positioning

锚点定位

CSS-native positioning of tooltips, popovers, and floating UI relative to a trigger.
css
.trigger {
  anchor-name: --my-trigger;
}

.tooltip {
  position: fixed;
  position-anchor: --my-trigger;
  position-area: top center;
  margin-bottom: 0.5rem;

  /* Fallback if no space on top */
  position-try-fallbacks: --bottom;
}

@position-try --bottom {
  position-area: bottom center;
  margin-top: 0.5rem;
}
Combine with
@starting-style
for animated tooltips:
css
.tooltip[popover]:popover-open {
  opacity: 1;
  transform: scale(1);
  transition: opacity 150ms ease, transform 150ms ease,
              display 150ms allow-discrete, overlay 150ms allow-discrete;

  @starting-style {
    opacity: 0;
    transform: scale(0.96);
  }
}

CSS原生实现提示框、弹出层和浮动UI相对于触发元素的定位。
css
.trigger {
  anchor-name: --my-trigger;
}

.tooltip {
  position: fixed;
  position-anchor: --my-trigger;
  position-area: top center;
  margin-bottom: 0.5rem;

  /* 顶部无空间时的回退方案 */
  position-try-fallbacks: --bottom;
}

@position-try --bottom {
  position-area: bottom center;
  margin-top: 0.5rem;
}
结合
@starting-style
实现带动画的提示框:
css
.tooltip[popover]:popover-open {
  opacity: 1;
  transform: scale(1);
  transition: opacity 150ms ease, transform 150ms ease,
              display 150ms allow-discrete, overlay 150ms allow-discrete;

  @starting-style {
    opacity: 0;
    transform: scale(0.96);
  }
}

Container Queries + Contextual Animations

容器查询 + 上下文动画

Adapt animations to the component's container size, not the viewport.
css
.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card-content {
    animation: slide-in-right 400ms var(--ease-out-expo);
  }
}

@container card (max-width: 399px) {
  .card-content {
    animation: fade-in 300ms ease;
  }
}
Container-relative units in keyframes:
css
@keyframes slide-in-right {
  from { transform: translateX(10cqw); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

根据组件的容器大小而非视口适配动画。
css
.card-container {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card-content {
    animation: slide-in-right 400ms var(--ease-out-expo);
  }
}

@container card (max-width: 399px) {
  .card-content {
    animation: fade-in 300ms ease;
  }
}
关键帧中的容器相对单位:
css
@keyframes slide-in-right {
  from { transform: translateX(10cqw); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}

Advanced Visual Techniques

高级视觉技术

clip-path Transitions

clip-path过渡

css
.reveal {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 600ms cubic-bezier(0.77, 0, 0.175, 1);
}
.reveal.visible {
  clip-path: inset(0 0 0 0);
}
Shape morphing: transition between
circle()
,
ellipse()
,
polygon()
,
inset()
— as long as the function type and point count match.
css
.reveal {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 600ms cubic-bezier(0.77, 0, 0.175, 1);
}
.reveal.visible {
  clip-path: inset(0 0 0 0);
}
形状变形:在
circle()
ellipse()
polygon()
inset()
之间过渡——只要函数类型和点数匹配即可。

backdrop-filter

backdrop-filter

css
.glass {
  background: oklch(0.98 0.01 250 / 0.6);
  backdrop-filter: blur(12px) saturate(1.8);
  -webkit-backdrop-filter: blur(12px) saturate(1.8); /* Safari */
}
css
.glass {
  background: oklch(0.98 0.01 250 / 0.6);
  backdrop-filter: blur(12px) saturate(1.8);
  -webkit-backdrop-filter: blur(12px) saturate(1.8); /* Safari */
}

mix-blend-mode

mix-blend-mode

css
.overlay-text {
  mix-blend-mode: difference;
  color: white; /* inverts over any background */
}
css
.overlay-text {
  mix-blend-mode: difference;
  color: white; /* 在任何背景上都会反转颜色 */
}

Mesh Gradients (approximated)

网格渐变(近似实现)

css
.mesh {
  background:
    radial-gradient(at 20% 30%, oklch(0.7 0.2 310) 0%, transparent 50%),
    radial-gradient(at 80% 60%, oklch(0.6 0.18 250) 0%, transparent 50%),
    radial-gradient(at 50% 80%, oklch(0.75 0.15 170) 0%, transparent 50%),
    oklch(0.15 0.02 280);
}
css
.mesh {
  background:
    radial-gradient(at 20% 30%, oklch(0.7 0.2 310) 0%, transparent 50%),
    radial-gradient(at 80% 60%, oklch(0.6 0.18 250) 0%, transparent 50%),
    radial-gradient(at 50% 80%, oklch(0.75 0.15 170) 0%, transparent 50%),
    oklch(0.15 0.02 280);
}

conic-gradient Effects

conic-gradient效果

css
.spinner {
  background: conic-gradient(from 0deg, transparent 0%, oklch(0.7 0.15 250) 100%);
  border-radius: 50%;
  mask: radial-gradient(farthest-side, transparent calc(100% - 4px), black calc(100% - 4px));
  animation: spin 1s linear infinite;
}

css
.spinner {
  background: conic-gradient(from 0deg, transparent 0%, oklch(0.7 0.15 250) 100%);
  border-radius: 50%;
  mask: radial-gradient(farthest-side, transparent calc(100% - 4px), black calc(100% - 4px));
  animation: spin 1s linear infinite;
}

Do Not

避坑指南

BADGOODWhy
transition: all 300ms
transition: opacity 300ms, transform 300ms
all
triggers transitions on every property change, causes unexpected animations, and prevents browser optimization
Animate
width
,
height
,
top
,
left
Animate
transform
,
opacity
,
clip-path
,
filter
Layout-triggering properties force reflow on every frame — composite-only properties run on GPU
Scroll-driven animations without fallback
@supports (animation-timeline: scroll()) { ... }
Firefox only added support in v128+, older Safari versions lack support
@starting-style
without
transition-behavior
Always pair with
allow-discrete
for display/overlay
Without it,
display: none
transitions are skipped entirely
Anchor positioning without
position-try-fallbacks
Always define fallback positionsElement clips out of viewport if primary position has no space
animation-fill-mode: forwards
on scroll-driven
Use
both
for scroll-driven animations
forwards
can lock the element in its final state even when scrolling back

错误做法正确做法原因
transition: all 300ms
transition: opacity 300ms, transform 300ms
all
会在每个属性变化时触发过渡,导致意外动画,且无法被浏览器优化
动画
width
height
top
left
动画
transform
opacity
clip-path
filter
触发布局的属性会在每一帧强制重排——仅合成属性在GPU上运行
滚动驱动动画不做兼容处理
@supports (animation-timeline: scroll()) { ... }
Firefox在v128+才添加支持,旧版Safari不支持
使用
@starting-style
但不搭配
transition-behavior
始终搭配
allow-discrete
处理display/overlay属性
没有它,
display: none
的过渡会被完全跳过
锚点定位不设置
position-try-fallbacks
始终定义回退位置如果主位置没有空间,元素会超出视口
滚动驱动动画使用
animation-fill-mode: forwards
滚动驱动动画使用
both
forwards
会将元素锁定在最终状态,即使滚动回去也不会恢复

Deep dive

深入学习

NeedReference
Browser-support tables, fallback patterns and progressive-enhancement strategies for scroll-driven animations, View Transitions,
@starting-style
, anchor positioning and container queries (plus accessibility and performance notes)
references/modern-css.md
需求参考资料
滚动驱动动画、View Transitions、
@starting-style
、锚点定位和容器查询的浏览器支持表、兼容方案和渐进增强策略(含可访问性和性能说明)
references/modern-css.md