CSS-in-JS vs Traditional CSS
Answer
CSS-in-JS is an approach where CSS is written within JavaScript files, providing component-scoped styles. It contrasts with traditional CSS approaches like separate stylesheets or CSS Modules.
Approach Comparison
Traditional CSS
/* styles.css */
.button {
background: blue;
color: white;
padding: 10px 20px;
}
.button:hover {
background: darkblue;
}
.button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
<link rel="stylesheet" href="styles.css" />
<button class="button">Click me</button>
Pros:
- Browser caching
- No runtime overhead
- Works without JavaScript
- Familiar to all developers
Cons:
- Global namespace (name collisions)
- Dead code is hard to detect
- No dynamic values from JavaScript
CSS Modules
/* Button.module.css */
.button {
background: blue;
color: white;
}
.disabled {
opacity: 0.5;
}
import styles from "./Button.module.css";
function Button({ disabled }) {
return (
<button className={`${styles.button} ${disabled ? styles.disabled : ""}`}>
Click me
</button>
);
}
Pros:
- Scoped class names (no collisions)
- Still separate CSS files
- Browser caching works
- No runtime overhead
Cons:
- Still need conditional class logic
- Limited dynamic styling
- Separate file per component
CSS-in-JS Libraries
Styled Components
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
opacity: ${props => props.disabled ? 0.5 : 1};
&:hover {
background: ${props => props.primary ? 'darkblue' : 'darkgray'};
}
`;
// Usage
<Button primary>Click me</Button>
<Button disabled>Disabled</Button>
Emotion
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
const buttonStyle = css`
background: blue;
color: white;
padding: 10px 20px;
`;
function Button() {
return <button css={buttonStyle}>Click me</button>;
}
// Or with styled API
import styled from "@emotion/styled";
const Button = styled.button`
background: blue;
`;
Comparison Table
| Feature | Traditional CSS | CSS Modules | CSS-in-JS |
|---|---|---|---|
| Scoping | Global | Scoped | Scoped |
| Dynamic styles | Limited | Limited | Full |
| Runtime cost | None | None | Yes |
| Bundle size | Separate | Hashed | In JS bundle |
| Dead code elimination | Hard | Better | Automatic |
| Theming | Variables | Variables | Props/Context |
| Learning curve | Low | Low | Medium |
| SSR complexity | Simple | Simple | More complex |
Modern Alternatives
Tailwind CSS (Utility-First)
<button
class="bg-blue-500 hover:bg-blue-700 text-white py-2 px-4 rounded disabled:opacity-50"
>
Click me
</button>
Pros:
- No custom CSS to write
- Consistent design system
- Small production bundle (purged)
Cons:
- HTML can become verbose
- Learning utility classes
- Less readable for complex styles
Zero-Runtime CSS-in-JS
// Vanilla Extract (zero runtime)
import { style } from "@vanilla-extract/css";
export const button = style({
background: "blue",
color: "white",
":hover": {
background: "darkblue",
},
});
Performance Considerations
CSS-in-JS Performance Issues:
- Style parsing at runtime
- CSS injection on every render (if not optimized)
- Larger JavaScript bundles
- Hydration mismatches in SSR
Mitigations:
- Use zero-runtime libraries (Vanilla Extract, Linaria)
- Static extraction when possible
- Memoization of style objects
When to Use What
| Scenario | Recommendation |
|---|---|
| Static site/content | Traditional CSS or Tailwind |
| Component library | CSS Modules or CSS-in-JS |
| Design system | Tailwind or CSS Variables |
| Highly dynamic UI | CSS-in-JS |
| Performance critical | Zero-runtime or Traditional |
| Team familiarity | What team knows best |
Key Points
- Traditional CSS: Simple, cacheable, global scope issues
- CSS Modules: Scoped, no runtime, still separate files
- CSS-in-JS: Dynamic, collocated, runtime overhead
- Tailwind: Utility, fast development, verbose HTML
- Zero-runtime: Best of both (scope + performance)
- Choose based on project needs, not trends