CSS Architecture: Tailwind vs CSS Modules vs CSS-in-JS
A practical guide to CSS architecture patterns — when to use Tailwind for rapid development, CSS Modules for component scoping, or CSS-in-JS for dynamic styling.
A practical guide to animations in React — CSS transitions and keyframes, Framer Motion for complex interactions, performance optimization, and accessibility considerations.
By codefunc
Animations make interfaces feel responsive and alive. A well-timed fade, a smooth transition, or a satisfying bounce provides feedback and guides attention. But animations done wrong — too slow, too distracting, or inaccessible — hurt more than they help. Understanding when to use CSS animations versus JavaScript libraries like Framer Motion is essential.
Bottom line: Use CSS transitions for simple state changes (hover, focus). Use CSS keyframes for looping or complex multi-step animations. Use Framer Motion for gesture-based interactions, layout animations, and coordinated sequences. Always respect prefers-reduced-motion. Animate transform and opacity for best performance — avoid animating width, height, or top/left.
Transitions animate changes between states.
.button {
background-color: #3b82f6;
transition: background-color 200ms ease;
}
.button:hover {
background-color: #2563eb;
}
.card {
transform: scale(1);
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition:
transform 200ms ease,
box-shadow 200ms ease;
}
.card:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* property | duration | timing-function | delay */
transition: transform 300ms ease-out 100ms;
/* All properties */
transition: all 200ms ease;
| Function | Description |
|---|---|
linear |
Constant speed |
ease |
Slow start, fast middle, slow end |
ease-in |
Slow start |
ease-out |
Slow end |
ease-in-out |
Slow start and end |
cubic-bezier() |
Custom curve |
/* Custom easing */
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
Keyframes define multi-step animations.
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.element {
animation: fadeIn 300ms ease forwards;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
}
.bouncing {
animation: bounce 600ms ease-in-out infinite;
}
/* name | duration | timing | delay | iterations | direction | fill-mode */
animation: slideIn 500ms ease-out 100ms 1 normal forwards;
/* Fade in up */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Pulse */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Spin */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Shake */
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
function Button({ isLoading }) {
return (
<button className={`btn ${isLoading ? 'btn-loading' : ''}`}>
{isLoading ? 'Loading...' : 'Submit'}
</button>
);
}
.btn {
transition: opacity 200ms ease;
}
.btn-loading {
opacity: 0.7;
pointer-events: none;
}
/* Button.module.css */
.button {
transition: transform 200ms ease;
}
.button:active {
transform: scale(0.95);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fadeIn {
animation: fadeIn 300ms ease;
}
Framer Motion provides declarative animations for React.
npm install framer-motion
import { motion } from 'framer-motion';
function FadeIn({ children }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
function Card() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: 'easeOut' }}
className="card"
>
Card content
</motion.div>
);
}
function Button({ children }) {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
{children}
</motion.button>
);
}
import { motion, AnimatePresence } from 'framer-motion';
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="modal-backdrop"
onClick={onClose}
>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ type: 'spring', damping: 25 }}
className="modal"
onClick={(e) => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
Define animation states for reuse and orchestration:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function List({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
);
}
Animate layout changes automatically:
function ExpandableCard({ isExpanded, onClick }) {
return (
<motion.div
layout
onClick={onClick}
style={{ borderRadius: 12 }}
>
<motion.h2 layout="position">Title</motion.h2>
<AnimatePresence>
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Expanded content here...
</motion.p>
)}
</AnimatePresence>
</motion.div>
);
}
Animate between different components:
function Tabs({ tabs, activeTab, setActiveTab }) {
return (
<div className="tabs">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'active' : ''}
>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeTab"
className="underline"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
/>
)}
</button>
))}
</div>
);
}
function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1 }}
>
Drag me
</motion.div>
);
}
import { motion, useScroll, useTransform } from 'framer-motion';
function ParallaxHero() {
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, 150]);
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return (
<motion.div style={{ y, opacity }} className="hero">
<h1>Parallax Hero</h1>
</motion.div>
);
}
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
function Section({ children }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<motion.section
ref={ref}
initial={{ opacity: 0, y: 50 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
{children}
</motion.section>
);
}
| Good (GPU accelerated) | Bad (triggers layout) |
|---|---|
transform |
width, height |
opacity |
top, left, right, bottom |
filter |
margin, padding |
/* Bad - triggers layout */
.element {
transition: width 300ms;
}
/* Good - GPU accelerated */
.element {
transition: transform 300ms;
transform: scaleX(1);
}
.element.expanded {
transform: scaleX(1.5);
}
will-change sparingly/* Only use when animation is imminent */
.element:hover {
will-change: transform;
}
/* Don't apply to everything */
* {
will-change: transform; /* Bad! */
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
// Framer Motion respects this automatically, or use hook
import { useReducedMotion } from 'framer-motion';
function AnimatedComponent() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
Content
</motion.div>
);
}
| Use case | Solution |
|---|---|
| Hover/focus states | CSS transitions |
| Loading spinners | CSS keyframes |
| Page transitions | Framer Motion |
| List reordering | Framer Motion layout |
| Scroll-linked effects | Framer Motion useScroll |
| Drag and drop | Framer Motion gestures |
| Complex sequences | Framer Motion variants |
| Simple fades | CSS transitions or Framer Motion |
Use CSS Minifier to optimize animation stylesheets. Generate border radius values with Border Radius Generator. Convert colors for animations with Color Converter.
Start with CSS transitions for simple state changes — they're performant and require no JavaScript. Use CSS keyframes for loading states and continuous animations. Reach for Framer Motion when you need exit animations, gesture interactions, or layout animations. Always animate transform and opacity for smoothness. Respect reduced motion preferences. Keep animations brief (200-500ms) and purposeful — animation should enhance UX, not distract from it.
A practical guide to CSS architecture patterns — when to use Tailwind for rapid development, CSS Modules for component scoping, or CSS-in-JS for dynamic styling.
A practical guide to authentication for frontend developers — understanding OAuth flows, JWT structure, session cookies, and implementing secure auth in React applications.
A practical guide to using GraphQL in frontend applications — writing queries and mutations, client-side caching with Apollo and urql, and patterns for React integration.
Find a developer tool