CSS Weekly #10: CSS-in-JS vs Utility-First CSS
CSS Weekly #10: CSS-in-JS vs Utility-First CSS ⚔️
Hey ,
For our 10th issue, let's tackle the great debate: CSS-in-JS or Utility-First CSS? I'll share my experience with both approaches and help you choose.
The Contenders
CSS-in-JS: Write styles in JavaScript (styled-components, Emotion) Utility-First: Compose styles with utility classes (Tailwind, UnoCSS)
CSS-in-JS Deep Dive
The Good ✅
// Component-scoped styles
const Button = styled.button`
background: ${props => props.primary ? '#0066cc' : '#gray'};
padding: 0.5rem 1rem;
&:hover {
transform: translateY(-2px);
}
${props => props.large && css`
padding: 1rem 2rem;
font-size: 1.25rem;
`}
`;
// Dynamic theming
const Card = styled.div`
background: ${({ theme }) => theme.colors.surface};
color: ${({ theme }) => theme.colors.text};
`;
Benefits: - True component isolation - Dynamic styles based on props - No class name conflicts - TypeScript support - Theme integration
The Challenges ❌
// Runtime overhead
const ExpensiveComponent = styled.div`
/* Styles parsed at runtime */
${generateComplexStyles()}
`;
// Server-side rendering complexity
// Larger bundle sizes
// Dev tools can be tricky
Utility-First Deep Dive
The Good ✅
<!-- Instant visual feedback -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
<h3 class="text-lg font-semibold text-gray-900">Card Title</h3>
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Action
</button>
</div>
<!-- Responsive utilities -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Cards -->
</div>
Benefits: - Tiny production CSS - No runtime overhead - Instant prototyping - Consistent spacing/colors - Great DX with IDE support
The Challenges ❌
<!-- "Ugly" HTML -->
<div class="relative flex min-h-screen flex-col justify-center overflow-hidden bg-gray-50 py-6 sm:py-12">
<!-- Long class strings -->
</div>
<!-- Component extraction needed -->
<style>
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}
</style>
🎯 My Hybrid Approach
I use both! Here's how:
1. Design System Components (CSS-in-JS)
// Complex, reusable components
const Select = styled.select`
${baseInputStyles}
appearance: none;
background-image: url('...');
&:focus {
${focusStyles}
}
`;
2. Layout & Utilities (Tailwind)
<!-- Page layouts and one-off styles -->
<main class="container mx-auto px-4 py-8">
<section class="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-2" />
<Sidebar />
</section>
</main>
3. Modern Alternative: CSS Modules + Utilities
/* Component.module.css */
.card {
composes: rounded-lg shadow-md from global;
/* Custom styles */
container-type: inline-size;
@container (min-width: 400px) {
display: grid;
grid-template-columns: 200px 1fr;
}
}
Real-World Comparison
Building a Card Component
CSS-in-JS:
const Card = styled.article`
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
${props => props.featured && css`
border: 2px solid var(--primary);
background: var(--primary-light);
`}
`;
<Card featured={isSpecial}>Content</Card>
Utility-First:
function Card({ featured, children }) {
return (
<article className={`
bg-white rounded-lg p-6 shadow-sm
${featured ? 'border-2 border-blue-500 bg-blue-50' : ''}
`}>
{children}
</article>
);
}
CSS Modules:
/* Card.module.css */
.card {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.featured {
border: 2px solid var(--primary);
background: var(--primary-light);
}
🚀 Performance Comparison
// Bundle size impact
CSS-in-JS: ~15-30kb runtime
Tailwind: ~10kb compressed (all utilities)
CSS Modules: 0kb runtime
// Runtime performance
CSS-in-JS: Style computation at runtime
Tailwind: Zero runtime overhead
CSS Modules: Zero runtime overhead
// Build time
CSS-in-JS: Fast builds
Tailwind: JIT compilation
CSS Modules: Standard CSS processing
Decision Framework
Choose CSS-in-JS when:
- Building a component library
- Need runtime style computation
- Strong TypeScript requirements
- Team prefers JS-centric workflow
Choose Utility-First when:
- Rapid prototyping needed
- Performance is critical
- Design consistency important
- Team includes designers
Choose CSS Modules when:
- Want zero runtime overhead
- Prefer writing actual CSS
- Need CSS features (container queries, layers)
- Building long-term maintainable apps
🎨 My Current Stack
{
"styling": {
"base": "CSS Modules",
"utilities": "Tailwind CSS",
"components": "CSS Modules + Tailwind @apply",
"themes": "CSS Custom Properties",
"animations": "Native CSS + Framer Motion"
}
}
The Future?
I'm excited about: - StyleX: Facebook's atomic CSS-in-JS - Vanilla Extract: Zero-runtime CSS-in-TypeScript - UnoCSS: Instant atomic CSS engine - Lightning CSS: Rust-based CSS processing
Final Thoughts
There's no "winner"—use the right tool for your needs:
- Start with utilities for rapid development
- Extract components when patterns emerge
- Use CSS-in-JS for complex interactions
- Optimize later based on real metrics
The best CSS is the one your team can maintain! 🎯
Thank you for being part of CSS Weekly! This concludes our 10-issue series. Want more? Let me know what topics you'd like to see in Season 2!
Special thanks to our sponsors who made this newsletter possible: DevTools Pro, CSS Scan, Polypane, Framer, Sizzy, Raycast, Speedlify, Modern CSS Solutions, and FontPair.
— Sarah from CSS Weekly
P.S. What's your CSS approach? Join the discussion in our Discord community!