动画与Keyframes #

基本概念 #

Emotion 提供了 keyframes 函数来定义 CSS 关键帧动画,让你可以在 JavaScript 中创建和管理动画效果。

keyframes 基本用法 #

定义动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const fadeIn = keyframes`
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
`

const FadeInBox = styled.div`
  animation: ${fadeIn} 1s ease-in-out;
  padding: 20px;
  background: #007bff;
  color: white;
`

多步骤动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const bounce = keyframes`
  0%, 20%, 50%, 80%, 100% {
    transform: translateY(0);
  }
  40% {
    transform: translateY(-30px);
  }
  60% {
    transform: translateY(-15px);
  }
`

const BouncingBall = styled.div`
  width: 50px;
  height: 50px;
  background: #007bff;
  border-radius: 50%;
  animation: ${bounce} 1s infinite;
`

复杂动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const pulse = keyframes`
  0% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.7);
  }
  
  70% {
    transform: scale(1.1);
    box-shadow: 0 0 0 10px rgba(0, 123, 255, 0);
  }
  
  100% {
    transform: scale(1);
    box-shadow: 0 0 0 0 rgba(0, 123, 255, 0);
  }
`

const PulseButton = styled.button`
  padding: 12px 24px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  animation: ${pulse} 2s infinite;
`

动画属性 #

完整动画属性 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const slideIn = keyframes`
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
`

const SlideInPanel = styled.div`
  padding: 20px;
  background: white;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  
  animation-name: ${slideIn};
  animation-duration: 0.5s;
  animation-timing-function: ease-out;
  animation-delay: 0s;
  animation-iteration-count: 1;
  animation-direction: normal;
  animation-fill-mode: forwards;
  animation-play-state: running;
`

简写形式 #

jsx
const AnimatedBox = styled.div`
  animation: ${slideIn} 0.5s ease-out forwards;
`

动态动画 #

基于 Props 的动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const spin = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`

const Spinner = styled.div`
  width: ${props => props.size || 40}px;
  height: ${props => props.size || 40}px;
  border: ${props => props.thickness || 4}px solid #f3f3f3;
  border-top: ${props => props.thickness || 4}px solid #007bff;
  border-radius: 50%;
  animation: ${spin} ${props => props.speed || 1}s linear infinite;
`

function App() {
  return (
    <>
      <Spinner />
      <Spinner size={60} thickness={6} speed={0.5} />
      <Spinner size={20} thickness={2} speed={2} />
    </>
  )
}

条件动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const shake = keyframes`
  0%, 100% { transform: translateX(0); }
  10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
  20%, 40%, 60%, 80% { transform: translateX(5px); }
`

const Input = styled.input`
  padding: 12px;
  border: 2px solid ${props => props.error ? '#dc3545' : '#ddd'};
  border-radius: 4px;
  animation: ${props => props.error ? `${shake} 0.5s ease-in-out` : 'none'};
`

function Form() {
  const [value, setValue] = useState('')
  const error = value.length > 0 && value.length < 3
  
  return (
    <Input
      value={value}
      onChange={e => setValue(e.target.value)}
      error={error}
    />
  )
}

组合动画 #

多个动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`

const slideUp = keyframes`
  from { transform: translateY(20px); }
  to { transform: translateY(0); }
`

const AnimatedCard = styled.div`
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  
  animation: 
    ${fadeIn} 0.3s ease-out,
    ${slideUp} 0.3s ease-out;
`

动画序列 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const expand = keyframes`
  from { width: 0; }
  to { width: 100%; }
`

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`

const ProgressBar = styled.div`
  height: 4px;
  background: #007bff;
  border-radius: 2px;
  
  animation: 
    ${expand} 2s ease-out forwards,
    ${fadeIn} 0.3s ease-out;
  
  & span {
    animation: ${fadeIn} 0.3s ease-out 2s forwards;
    opacity: 0;
  }
`

过渡效果 #

基本过渡 #

jsx
import styled from '@emotion/styled'

const Button = styled.button`
  padding: 12px 24px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
  
  &:hover {
    background: #0056b3;
    transform: translateY(-2px);
  }
  
  &:active {
    transform: translateY(0);
  }
`

多属性过渡 #

jsx
import styled from '@emotion/styled'

const Card = styled.div`
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  
  transition: 
    transform 0.3s ease,
    box-shadow 0.3s ease,
    background-color 0.3s ease;
  
  &:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
  }
`

动态过渡 #

jsx
import styled from '@emotion/styled'

const AnimatedBox = styled.div`
  width: 100px;
  height: 100px;
  background: #007bff;
  border-radius: ${props => props.rounded ? '50%' : '8px'};
  
  transition: 
    border-radius ${props => props.duration || '0.3s'} ease,
    background-color ${props => props.duration || '0.3s'} ease;
  
  &:hover {
    background: #0056b3;
  }
`

常用动画示例 #

加载动画 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const dotBounce = keyframes`
  0%, 80%, 100% {
    transform: scale(0);
  }
  40% {
    transform: scale(1);
  }
`

const LoadingDots = styled.div`
  display: flex;
  gap: 8px;
  
  & span {
    width: 12px;
    height: 12px;
    background: #007bff;
    border-radius: 50%;
    animation: ${dotBounce} 1.4s ease-in-out infinite both;
    
    &:nth-child(1) { animation-delay: -0.32s; }
    &:nth-child(2) { animation-delay: -0.16s; }
    &:nth-child(3) { animation-delay: 0s; }
  }
`

function Loading() {
  return (
    <LoadingDots>
      <span></span>
      <span></span>
      <span></span>
    </LoadingDots>
  )
}

淡入淡出 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`

const fadeOut = keyframes`
  from { opacity: 1; }
  to { opacity: 0; }
`

const FadeContainer = styled.div`
  animation: ${props => props.visible ? fadeIn : fadeOut} 0.3s ease forwards;
`

function FadeWrapper({ children, visible }) {
  if (!visible) return null
  
  return (
    <FadeContainer visible={visible}>
      {children}
    </FadeContainer>
  )
}

滑动效果 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const slideInLeft = keyframes`
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
`

const slideInRight = keyframes`
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
`

const slideInUp = keyframes`
  from {
    transform: translateY(100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
`

const slideInDown = keyframes`
  from {
    transform: translateY(-100%);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
`

const SlidePanel = styled.div`
  padding: 20px;
  background: white;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  
  animation: ${props => {
    switch (props.direction) {
      case 'left': return slideInLeft
      case 'right': return slideInRight
      case 'up': return slideInUp
      case 'down': return slideInDown
      default: return slideInLeft
    }
  }} 0.3s ease forwards;
`

缩放效果 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const scaleIn = keyframes`
  from {
    transform: scale(0);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
`

const scaleOut = keyframes`
  from {
    transform: scale(1);
    opacity: 1;
  }
  to {
    transform: scale(0);
    opacity: 0;
  }
`

const Modal = styled.div`
  padding: 24px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  
  animation: ${props => props.open ? scaleIn : scaleOut} 0.3s ease forwards;
`

动画库集成 #

使用动画变量 #

jsx
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'

const animations = {
  fadeIn: keyframes`
    from { opacity: 0; }
    to { opacity: 1; }
  `,
  
  slideUp: keyframes`
    from { transform: translateY(20px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
  `,
  
  pulse: keyframes`
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.05); }
  `,
  
  spin: keyframes`
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  `,
}

const AnimatedComponent = styled.div`
  animation: ${props => animations[props.animation] || animations.fadeIn} 0.3s ease;
`

性能优化 #

使用 transform 和 opacity #

优先使用 transformopacity 实现动画,它们不会触发重排:

jsx
const GoodAnimation = styled.div`
  transition: transform 0.3s ease, opacity 0.3s ease;
  
  &:hover {
    transform: translateY(-4px);
    opacity: 0.9;
  }
`

will-change #

对于复杂动画,使用 will-change 提示浏览器:

jsx
const ComplexAnimation = styled.div`
  will-change: transform, opacity;
  animation: ${complexKeyframes} 1s ease;
`

下一步 #

掌握了动画与 Keyframes 后,继续学习 SSR服务端渲染,了解如何在服务端渲染中使用 Emotion。

最后更新:2026-03-28