CSS Weekly

Subscribe
Archives
August 23, 2025

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:

  1. Start with utilities for rapid development
  2. Extract components when patterns emerge
  3. Use CSS-in-JS for complex interactions
  4. 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!

Share CSS Weekly | Sponsor Season 2 | Unsubscribe

Don't miss what's next. Subscribe to CSS Weekly:
Start the conversation:
Powered by Buttondown, the easiest way to start and grow your newsletter.