CSS Animation Timeline Editor: From Keyframes to Smooth Animations#

Last week, I was building a landing page and the product team asked for “lively” animations. I thought writing a few CSS keyframes would be simple, but after 30 minutes of tweaking values and refreshing the page, the timing still felt off. So I built a visual editor to see changes in real-time.

The Core: CSS Keyframes#

CSS animations are essentially a series of keyframes that the browser interpolates between:

@keyframes bounce {
  0% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-30px);
  }
  100% {
    transform: translateY(0);
  }
}

.element {
  animation: bounce 1s ease infinite;
}

@keyframes defines the “script”, while animation controls the “performance”—duration, timing, iterations, etc.

The pain point with hand-coding: you can’t see the result. Change a value, refresh, wrong again, change, refresh… This loop is incredibly inefficient.

Designing a Visual Editor#

Three core features:

  1. Timeline: Visualize keyframe positions, click to add new frames
  2. Property Panel: Edit transform, opacity, background color for selected frame
  3. Live Preview: Changes take effect immediately, no refresh needed

Timeline Implementation#

The timeline is a 0-100% progress bar, with each keyframe as a draggable dot:

interface KeyframeData {
  offset: number           // 0-100, keyframe position
  translateX: number
  translateY: number
  scale: number
  rotate: number
  opacity: number
  backgroundColor: string
}

Clicking on empty timeline space snaps to the nearest 5% mark:

const handleTimelineClick = (e: React.MouseEvent<HTMLDivElement>) => {
  const rect = e.currentTarget.getBoundingClientRect()
  const x = e.clientX - rect.left
  const percent = Math.round((x / rect.width) * 20) * 5  // Snap to 5% increments
  const clamped = Math.max(0, Math.min(100, percent))
  
  if (!keyframes.some(k => k.offset === clamped)) {
    addKeyframe(clamped)
  }
}

CSS Code Generation#

The editor’s core value is generating usable CSS code. Keyframe generation logic:

const generateKeyframesCSS = (keyframes: KeyframeData[]) => {
  const sorted = [...keyframes].sort((a, b) => a.offset - b.offset)
  
  const lines = sorted.map(k => {
    const transforms: string[] = []
    
    if (k.translateX !== 0 || k.translateY !== 0) {
      transforms.push(`translateX(${k.translateX}px) translateY(${k.translateY}px)`)
    }
    if (k.scale !== 1) transforms.push(`scale(${k.scale})`)
    if (k.rotate !== 0) transforms.push(`rotate(${k.rotate}deg)`)
    
    const props: string[] = []
    if (transforms.length > 0) {
      props.push(`    transform: ${transforms.join(' ')};`)
    }
    if (k.opacity !== 1) {
      props.push(`    opacity: ${k.opacity};`)
    }
    props.push(`    background-color: ${k.backgroundColor};`)
    
    return `  ${k.offset}% {\n${props.join('\n')}\n  }`
  })
  
  return `@keyframes myAnimation {\n${lines.join('\n')}\n}`
}

Note the transform order: translatescalerotate. Different orders produce different results.

Animation Shorthand#

animation is a shorthand property combining multiple sub-properties:

const animationShorthand = () => {
  let val = `${animationName} ${duration}s ${timingFunction}`
  if (delay > 0) val += ` ${delay}s`
  val += ` ${iterationCount} ${direction}`
  if (fillMode !== 'none') val += ` ${fillMode}`
  return val
}

Full syntax: animation: name duration timing-function delay iteration-count direction fill-mode

Common pitfalls:

  • timing-function comes before delay: Wrong order causes parsing errors
  • fill-mode matters: forwards keeps the last frame, backwards shows the first frame during delay
  • direction’s alternate: Combine with infinite for back-and-forth playback

Built-in Presets#

I included several common animation presets for quick starts:

const presetAnimations = {
  bounce: {
    keyframes: [
      { offset: 0, translateY: 0, scale: 1 },
      { offset: 25, translateY: -40, scale: 1.1 },
      { offset: 50, translateY: 0, scale: 1 },
      { offset: 75, translateY: -20, scale: 1.05 },
      { offset: 100, translateY: 0, scale: 1 },
    ],
    duration: 1,
    timingFunction: 'ease',
  },
  // ... other presets
}

Presets based on real-world use cases:

  • bounce: Bouncing effect, great for buttons and icons
  • pulse: Pulsing effect, ideal for alerts and notifications
  • shake: Shaking effect, perfect for error feedback
  • fadeIn: Fade-in effect, works well for page loads

Performance Tips#

CSS animations are generally performant, but some details matter:

1. Only Animate transform and opacity#

These properties don’t trigger reflow/repaint and are GPU-accelerated:

/* Good */
.element {
  transform: translateX(100px);
  opacity: 0.5;
}

/* Bad */
.element {
  left: 100px;
  background-color: rgba(0, 0, 0, 0.5);
}

2. Use will-change to Hint the Browser#

.element {
  will-change: transform, opacity;
}

But don’t overuse it—only apply to elements that truly need it.

3. Avoid Animating Too Many Elements#

Each animated element consumes GPU resources. If you have dozens of animated elements, consider using IntersectionObserver to only animate visible ones.

Real-World Application#

Last week, I used this tool for a landing page animation:

  1. Selected slideIn preset, adjusted translateX from -100 to 0
  2. Added a keyframe at 30%, set opacity: 0.5 for a fade-in effect
  3. Adjusted duration to 0.6s, changed timing-function to ease-out
  4. Copied the generated CSS code into the project

The whole process took 5 minutes, and the result was better than hours of manual tweaking.

Try It Out#

Online tool: CSS Animation Timeline

Features:

  • Visual timeline editing
  • 6 built-in animation presets
  • Real-time preview
  • One-click CSS code copy
  • Support for all animation properties

Related tools: CSS Gradient Generator | Loading Generator