Let's build a silky-smooth, hardware-accelerated component animation system that won't drop frames on mobile devices.
What you'll learn
- How to structure custom Tailwind animations using pure CSS keyframes.
- Why sticking to
transformandopacitykeeps your UI at 60fps. - How to respect user preferences with
prefers-reduced-motion. - Managing staggered entry animations cleanly inside React.
Setting Up Custom Keyframes in Tailwind
Relying on JavaScript physics libraries for simple UI motion often leads to main-thread jank. Instead, we configure custom keyframes directly in Tailwind to let the browser's compositor thread handle the heavy lifting.
// tailwind.config.js
module.exports = {
theme: {
extend: {
keyframes: {
floatUp: {
'0%': { opacity: '0', transform: 'translateY(16px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
animation: {
'float-up': 'floatUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards',
},
},
},
};
Speed Note: Always use
cubic-beziercurves that mimic real-world physics. Easing out quickly (0.16, 1, 0.3, 1) makes UI elements feel instantly responsive.
Building a Reusable Animated Card Component
Now let's apply our custom animation to a real React component. We will keep our markup simple and pass down animation utility classes cleanly.
// components/AnimatedCard.tsx
import React from 'react';
interface AnimatedCardProps {
title: string;
description: string;
delay?: string;
}
export const AnimatedCard: React.FC<AnimatedCardProps> = ({
title,
description,
delay = '0ms'
}) => {
return (
<div
style={{ animationDelay: delay }}
className="p-6 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-sm opacity-0 animate-float-up hover:shadow-md transition-shadow"
>
<h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">{title}</h3>
<p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">{description}</p>
</div>
);
};
Gotcha: Setting
opacity-0initially prevents layout flashes while waiting for the component mount frame to trigger the animation.
Staggering List Items for Polished Motion
When rendering lists, animating everything at once looks chaotic. Staggering the entry delays creates an intentional, professional motion hierarchy.
// components/CardList.tsx
import React from 'react';
import { AnimatedCard } from './AnimatedCard';
const ITEMS = [
{ id: 1, title: 'Vector Graphics', desc: 'Optimizing SVG assets for web.' },
{ id: 2, title: 'Keyframe Control', desc: 'Fine-tuning timing functions.' },
{ id: 3, title: 'Compositor Threads', desc: 'Avoiding layout thrashing.' },
];
export const CardList: React.FC = () => {
return (
<div className="grid gap-4 max-w-md mx-auto">
{ITEMS.map((item, index) => (
<AnimatedCard
key={item.id}
title={item.title}
description={item.desc}
delay={`${index * 75}ms`}
/>
))}
</div>
);
};
Respecting Accessibility with Reduced Motion
Not all users want or can handle heavy UI motion. Always use the motion-reduce Tailwind modifier to disable or simplify animations for users with vestibular disorders.
// components/AccessibleWrapper.tsx
import React from 'react';
export const AccessibleBox: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<div className="transition-transform duration-300 transform hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:transform-none">
{children}
</div>
);
};
Accessibility First: Testing your app with
prefers-reduced-motion: reduceenabled in your OS settings is a mandatory check before shipping any motion feature.
Summary
By leveraging CSS keyframes inside Tailwind, we keep our bundle size tiny and ensure animations run entirely on the GPU compositor thread. Staggering delays gives interfaces a high-end feel without touching heavy JS animation runtimes.
Next Steps
- Add exit transitions using React state or layout transition hooks.
- Explore scroll-linked micro-interactions using native Intersection Observers.
- Test performance profiling in Chrome DevTools to ensure zero layout shifts.