local-link/skills/css-dev-skills/skills/css-animate/SKILL.md
Create performant CSS animations using composited properties, scroll-driven animations, View Transitions, and @keyframes choreography. Always includes prefers-reduced-motion fallback. Use when the user asks for CSS animation, transition, scroll animation, view transition, keyframes, hover effect, entrance animation, or motion design.
npx skillsauth add lionad-morotar/simple-local-llm-server css-animateInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a senior CSS animation engineer. Create performant, accessible animations using modern CSS. Always generate code. Every animation you produce must be composited-safe and include a prefers-reduced-motion fallback. For reference, see modern-patterns.md and browser-compat.md.
transform, opacity, and filter are animated.What kind of motion?
│
├─ State change (hover, focus, active, class toggle)?
│ └─ CSS transition
│ ├─ Simple → transition shorthand
│ └─ Staggered → transition-delay per element
│
├─ Complex multi-step sequence?
│ └─ @keyframes
│ ├─ Looping → animation-iteration-count: infinite
│ ├─ One-shot → animation-fill-mode: forwards
│ └─ Choreographed → stagger with animation-delay or custom properties
│
├─ Scroll-linked effect?
│ └─ Scroll-driven animation
│ ├─ Page scroll progress → animation-timeline: scroll()
│ └─ Element enters/exits viewport → animation-timeline: view()
│
├─ Page or state transition?
│ └─ View Transition API
│ ├─ SPA navigation → document.startViewTransition()
│ └─ MPA navigation → @view-transition { navigation: auto; }
│
└─ User preference?
└─ Always wrap in prefers-reduced-motion
Only animate these properties — they run on the GPU compositor and avoid layout/paint:
| Safe to animate | Triggers |
|----------------|----------|
| transform | Compositor only |
| opacity | Compositor only |
| filter | Compositor only (some filters) |
| clip-path | Paint only (acceptable for reveals) |
| background-color | Paint only (acceptable for color shifts) |
These trigger layout recalculation every frame:
width, height, min-width, max-widthtop, right, bottom, leftmargin, paddingborder-widthfont-sizeUse transform: scale() instead of width/height. Use transform: translate() instead of top/left.
.element:hover { will-change: transform; }
.element.is-animating {
will-change: transform;
animation: slide 300ms ease;
}
will-change only on elements about to animate, not globally.* { will-change: transform; }..button {
transition: transform 200ms ease, opacity 200ms ease, box-shadow 200ms ease;
&:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
&:active {
transform: translateY(0);
transition-duration: 100ms;
}
}
| Easing | When to use |
|--------|-------------|
| ease-out | Elements entering the screen |
| ease-in | Elements leaving the screen |
| ease-in-out | Elements moving from one position to another |
| cubic-bezier(0.34, 1.56, 0.64, 1) | Springy overshoot |
| cubic-bezier(0.22, 0.61, 0.36, 1) | Smooth deceleration |
| cubic-bezier(0.68, -0.55, 0.27, 1.55) | Elastic snap |
| linear | Scroll-driven or physics-based only |
CSS doesn't have native spring physics, but you can approximate:
.spring {
transition: transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.spring-soft {
transition: transform 600ms cubic-bezier(0.22, 1.36, 0.55, 1);
}
.spring-stiff {
transition: transform 300ms cubic-bezier(0.68, -0.3, 0.27, 1.3);
}
.list-item {
--_index: 0;
opacity: 0;
transform: translateY(1rem);
transition: opacity 300ms ease-out, transform 300ms ease-out;
transition-delay: calc(var(--_index) * 50ms);
}
.list-item.is-visible {
opacity: 1;
transform: translateY(0);
}
Set --_index per element via inline style or :nth-child():
.list-item:nth-child(1) { --_index: 0; }
.list-item:nth-child(2) { --_index: 1; }
.list-item:nth-child(3) { --_index: 2; }
/* or use inline style="--_index: N" */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(1rem);
}
}
.enter {
animation: fade-in-up 400ms ease-out;
}
@keyframes pulse {
50% { transform: scale(1.05); }
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes shimmer {
to { background-position: -200% center; }
}
.skeleton {
background: linear-gradient(
90deg,
oklch(90% 0 0) 0%,
oklch(95% 0 0) 40%,
oklch(90% 0 0) 80%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
.hero-title {
animation: fade-in-up 600ms ease-out both;
animation-delay: 0ms;
}
.hero-subtitle {
animation: fade-in-up 600ms ease-out both;
animation-delay: 150ms;
}
.hero-cta {
animation: fade-in-up 600ms ease-out both;
animation-delay: 300ms;
}
.progress-bar {
position: fixed;
inset-block-start: 0;
inset-inline: 0;
block-size: 3px;
background: var(--color-primary);
transform-origin: inline-start;
animation: grow-width linear both;
animation-timeline: scroll();
}
@keyframes grow-width {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@keyframes reveal {
from {
opacity: 0;
transform: translateY(2rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
.parallax-layer {
animation: parallax linear both;
animation-timeline: scroll();
}
@keyframes parallax {
to { transform: translateY(-30%); }
}
Scroll-driven animations: Chrome 115+, Safari 18+, Firefox 110+ (flag). For non-supporting browsers, elements are visible by default because the keyframe end state matches the natural state. Wrap in @supports if needed:
@supports (animation-timeline: scroll()) {
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 200ms;
}
.hero-image { view-transition-name: hero; }
.page-title { view-transition-name: title; }
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}
@keyframes slide-from-right {
from { transform: translateX(100%); }
}
@keyframes slide-to-left {
to { transform: translateX(-100%); }
}
::view-transition-old(root) {
animation: slide-to-left 300ms ease-in both;
}
::view-transition-new(root) {
animation: slide-from-right 300ms ease-out both;
}
Every animation you write MUST include a reduced-motion fallback.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
.card {
animation: fade-in-up 400ms ease-out;
@media (prefers-reduced-motion: reduce) {
animation: fade-in 200ms ease-out;
}
}
@keyframes fade-in {
from { opacity: 0; }
}
reduce.@supports fallback ensures content is visible without animation.prefers-reduced-motion by shortening duration.@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01ms;
}
}
transform, opacity, filter are animated (no layout properties)prefers-reduced-motion fallback is presentwill-change is scoped, not globalease everywhere)view-transition-name on shared elementstools
open understand dashboard for user
tools
这是一个技能文件的模板,展示了技能的基本结构和内容组织方式。
development
Be direct and informative. No filler, no fluff, but give enough to be useful.
development
使用 Evaluator-optimizer 模式进行系统性多轮网络搜索,采用结构化 Ask 流程在搜索前澄清研究目标。基于 YC Office Hours 的提问方法论,确保搜索方向清晰、结果可验证。当用户需要深入调查复杂主题、验证假设或全面收集信息时使用。