CSS Animation Timeline Editor: From Keyframes to Smooth Animations
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:
- Timeline: Visualize keyframe positions, click to add new frames
- Property Panel: Edit transform, opacity, background color for selected frame
- 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: translate → scale → rotate. 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:
forwardskeeps the last frame,backwardsshows the first frame during delay - direction’s alternate: Combine with
infinitefor 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:
- Selected slideIn preset, adjusted translateX from -100 to 0
- Added a keyframe at 30%, set opacity: 0.5 for a fade-in effect
- Adjusted duration to 0.6s, changed timing-function to ease-out
- 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