React: React Styling Solutions
Last updated: 2026-08-26
While maintaining a large e-commerce back-end project, Tom noticed that as the team grew, classe name conflicts in the global CSS were becoming increasingly frequent. Changing the style of a single button could affect the display on three different pages. He needed a solution that could “isolate” styles so they wouldn’t interfere with one another.
1. What You'll Learn
- How CSS Modules Work and Their Modular Isolation Mechanism
- The Design Philosophy Behind Tailwind CSS's Atomic Class Names
- styled-components' runtime CSS-in-JS solution
- Selection Strategies for the Three Options Based on Project Size
2. Conceptual Diagrams
The figure below illustrates the key differences among the three design approaches in terms of "writing style" and "runtime behavior":
flowchart LR
A[React Design Concepts] --> B[CSS Modules]
A --> C[Tailwind CSS]
A --> D[styled-components]
B --> B1["*.module.css Documents"]
B --> B2["Compile to Generate a Unique Hash Class Name"]
B --> B3["Natural Barrier / Zero Runtime"]
C --> C1["Tool Sets className"]
C --> C2["PurgeCSS Remove unused styles"]
C --> C3["Build at Compile Time / Zero Runtime"]
D --> D1["JS Styling Template Strings"]
D --> D2["Runtime Injection style Tags"]
D --> D3["Props Driving Dynamic Styles"]
style B fill:#e3f2fd,stroke:#1565c0
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
3. A Real-Life Scenario
| Styling Approach | Isolation | Runtime Overhead | Dynamic Styling | SSR Compatibility |
|---|---|---|---|---|
| Inline Styles | No Isolation | None | ✅ Write JS Directly | ✅ |
| CSS Modules | Hash Isolation | None | ❌ Requires concatenating className | ✅ |
| Tailwind CSS | No minification (PurgeCSS) | None | ⚠️ Conditional class name combinations | ✅ |
| styled-components | Unique class name isolation | Yes (runtime injection) | ✅ Props-driven | ⚠️ Requires SSR configuration |
Tom's team maintains a backend system with over 50 pages. In the early days, they used a global CSS file, and class naming conventions were based entirely on BEM. As five front-end developers began working on the project simultaneously, the following issues arose:
- Style Conflict:
.rowinTable.cssaffects.rowinOrderList.css - Severe coupling: Changing the styling of a shared component requires testing all pages that use it
- Style Leak: Styles defined on Page A unexpectedly override elements on Page B
Tom decided to adopt an engineering-driven styling approach to address these issues. He first tried CSS Modules, then tested Tailwind CSS, and ultimately built the design system using styled-components. Here is a complete walkthrough of all three approaches.
(1) CSS Modules — Zero-Cost Style Isolation
CSS Modules are the easiest styling isolation solution to get started with in React projects. They compile each CSS file into a module with a unique hashed class name, fundamentally eliminating class name conflicts.
Core Principle: When you write .button, it compiles to .Button_button_abc123, so other components cannot accidentally hit it.
Writing CSS Module Files
/* Button.module.css */
.button {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.primary {
background: #1890ff;
color: white;
}
.primary:hover {
background: #40a9ff;
}
.danger {
background: #ff4d4f;
color: white;
}
.danger:hover {
background: #ff7875;
}
.default {
background: #f0f0f0;
color: #333;
}
.default:hover {
background: #d9d9d9;
}
Importing and Using in Components
import styles from './Button.module.css'
function Button({ variant = 'primary', children, onClick }) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{children}
</button>
)
}
export default Button
Compiled DOM output:
<button class="Button_button_1a2b3c Button_primary_4d5e6f">Submit</button>; the class name is globally unique.
Best Practices for Dynamic Class Name Concatenation
When a component needs to generate class names based on a combination of multiple conditions, we recommend using the clsx or classnames libraries to avoid excessively deep template string nesting:
npm install clsx
import styles from './Button.module.css'
import clsx from 'clsx'
function Button({ variant = 'primary', size = 'medium', disabled, children }) {
return (
<button
className={clsx(
styles.button,
styles[variant],
styles[size],
{ [styles.disabled]: disabled }
)}
disabled={disabled}
>
{children}
</button>
)
}
/* Button.module.css — Add New Size Variants */
.small { padding: 4px 12px; font-size: 12px; }
.medium { padding: 8px 20px; font-size: 14px; }
.large { padding: 12px 28px; font-size: 16px; }
.disabled { opacity: 0.5; cursor: not-allowed; }
▶ Example 1: Combining Multiple Class Names with CSS Modules
Output:
Styled components with CSS-in-JS or scoped styles
import styles from './Card.module.css'
function Card({ title, children, isHighlighted }) {
return (
<div
className={
`${styles.card} ${isHighlighted ? styles.highlighted : ''}`
}
>
<h3 className={styles.title}>{title}</h3>
<div className={styles.content}>{children}</div>
</div>
)
}
Output:
CSS Modules: import styles from "./Button.module.css". Class names are scoped: <button className={styles.primary + " " + styles.large}>
/* Card.module.css */
.card {
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 16px;
background: #fff;
}
.highlighted {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #1a1a1a;
}
.content {
font-size: 14px;
color: #666;
line-height: 1.6;
}
Suitable Scenarios: Small- to medium-sized projects; teams with many new members; projects seeking style isolation at the lowest possible cost.
(2) Tailwind CSS — Atomic, Rapid Development
Tailwind CSS provides a set of atomic (utility-first) CSS class names. Developers don’t need to write custom CSS; instead, they build the UI by combining atomic classes in JSX.
Key Idea: Instead of naming classes, use semantically clear utility classes such as p-4 (padding: 16px), text-lg (font-size: 18px), and bg-blue-500.
Installation and Configuration
npm install -D tailwindcss @tailwindcss/vite
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
/* index.css */
@import "tailwindcss";
▶ Example 2: Building a Card Component with Tailwind CSS
Output:
Product card: image, name, description, price ($xx.xx), "Add to Cart" button. Tailwind utility classes for responsive layout
function ProductCard({ product, onAddToCart }) {
return (
<div className="border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow bg-white">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-md mb-3"
/>
<h3 className="text-lg font-semibold text-gray-800 mb-1">
{product.name}
</h3>
<p className="text-sm text-gray-500 mb-2 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-blue-600">
${product.price}
</span>
<button
onClick={() => onAddToCart(product)}
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Add to Cart
</button>
</div>
</div>
)
}
Output:
Renders:
{product.name}
+
{product.description}
Advantages of Tailwind:
- No need to write CSS files, reducing the need to switch between files
- Class names serve as style descriptions and are highly readable
- When used with PurgeCSS, it can significantly reduce the size of the output
- For responsive design, simply add the prefix:
md:flex,lg:grid-cols-3
Use Cases: Rapid prototyping, startup projects, and medium- to large-scale projects where teams adopt a unified atomic design approach.
(3) styled-components — CSS-in-JS Design System
styled-components is a leading implementation of CSS-in-JS, where styles are written using template strings in JavaScript, and the styles are tightly coupled with the components.
Installation
npm install styled-components
▶ Example 3: The styled-components Themed Button System
Output:
Displays: "Add to Cart". Button: onAddToCart(product)} className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors" > Add to Cart
import styled, { ThemeProvider } from 'styled-components'
// Define the Topic
const theme = {
colors: {
primary: '#1890ff',
success: '#52c41a',
danger: '#ff4d4f',
text: '#333',
textLight: '#fff',
},
radii: {
sm: '4px',
md: '8px',
lg: '12px',
},
fonts: {
body: "'Inter', sans-serif",
},
}
// Styled Components — Automatic Receipt theme prop
const StyledButton = styled.button`
padding: 10px 24px;
border: none;
border-radius: ${props => props.theme.radii.md};
font-family: ${props => props.theme.fonts.body};
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
/* props Conditional Styles Driven by Rules */
background: ${props => {
if (props.$variant === 'danger') return props.theme.colors.danger
if (props.$variant === 'success') return props.theme.colors.success
return props.theme.colors.primary
}};
color: ${props => props.theme.colors.textLight};
&:hover {
opacity: 0.85;
transform: translateY(-1px);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
`
const ButtonGroup = styled.div`
display: flex;
gap: 12px;
padding: 20px;
`
function App() {
return (
<ThemeProvider theme={theme}>
<ButtonGroup>
<StyledButton $variant="primary">Main Buttons</StyledButton>
<StyledButton $variant="success">Success Button</StyledButton>
<StyledButton $variant="danger" disabled>Danger(Disable)</StyledButton>
</ButtonGroup>
</ThemeProvider>
)
}
Output:
❌ Hooks inside conditions cause bugs. ✅ Always call hooks at top level. Rule: Hooks must be called in same order every render.
Key Features of styled-components:
- Styles are tightly coupled with components, so conflicts naturally do not arise
ThemeProviderProvides global themes to make it easy to switch between design systems- Props can be used to implement dynamic logic in styles
- Supports
&nested syntax, animation keyframes, and global stylescreateGlobalStyle
Use Cases: Large-scale design systems, scenarios requiring dynamic theme switching, and UI component library development.
4. Comparison of Solution Options
| Dimension | CSS Modules | Tailwind CSS | styled-components |
|---|---|---|---|
| Learning Curve | Low (Standard CSS) | Medium (Requires memorizing class names) | Medium (Requires understanding CSS-in-JS) |
| Style Isolation | Full Isolation (Hash) | None (Global Class Names) | Full Isolation (Unique Class Names) |
| Runtime overhead | None | None | Yes (injects a style tag) |
| Dynamic Styles | Concatenating via JS className |
Conditional Class Name Combination | Directly Driven by Props |
| Theme System | Requires an additional solution | CSS variables + configuration | Built into ThemeProvider |
| Build Output | Separate CSS File | Minimal Size After PurgeCSS | Included in JS Bundle |
| Optimal Project Size | Small to Medium | Medium to Large | Large/Design System |
5. Recommendations for Model Selection
| Team/Project Details | Recommended Solution |
|---|---|
| Teams of 2–5 people, small to medium-sized projects | CSS Modules (the most cost-effective way to achieve isolation) |
| Startup teams need rapid iteration | Tailwind CSS (no need to write CSS, faster development) |
| Large-scale enterprise projects, system design | styled-components (comprehensive theme system) |
| Migration of Existing Projects | Keep the current approach and gradually replace new modules with CSS Modules |
| Component Library Development | styled-components or CSS Modules (either is acceptable) |
▶ Example 4: CSS Modules Component Styles
Output:
Displays: "Main Buttons"
// Button.module.css
// .primary { background: #1677ff; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; }
// .danger { background: #ff4d4f; color: white; }
// .outline { background: transparent; border: 1px solid #1677ff; color: #1677ff; }
// .small { padding: 4px 8px; font-size: 12px; }
// .disabled { opacity: 0.5; cursor: not-allowed; }
import styles from './Button.module.css'
function Button({ variant = 'primary', size, disabled, children, ...props }) {
const classNames = [
styles[variant],
size === 'sm' && styles.small,
disabled && styles.disabled,
].filter(Boolean).join(' ')
return (
<button className={classNames} disabled={disabled} {...props}>
{children}
</button>
)
}
function ButtonDemo() {
return (
<div style={{ display: 'flex', gap: 8 }}>
<Button>Primary</Button>
<Button variant="danger">Delete</Button>
<Button variant="outline" size="sm">Small Outline</Button>
<Button disabled>Disabled</Button>
</div>
)
}
Output:
CSS Modules: .card { ... } → unique class name .card_abc123. No global conflicts. Import as styles object: className={styles.card}
▶ Example 5: Responsive Card Grid with Tailwind CSS
Output:
Displays: "Primary". Button: {children}
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 p-4">
{products.map(product => (
<div key={product.id} className="bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 overflow-hidden">
<div className="h-48 bg-gray-200 flex items-center justify-center">
<span className="text-gray-400 text-4xl">📦</span>
</div>
<div className="p-4">
<h3 className="font-semibold text-gray-800 truncate">{product.name}</h3>
<p className="text-sm text-gray-500 mt-1">{product.category}</p>
<div className="flex items-center justify-between mt-3">
<span className="text-lg font-bold text-red-500">${product.price}</span>
<button className="bg-blue-500 hover:bg-blue-600 text-white text-sm px-3 py-1 rounded transition-colors">
Add to Cart
</button>
</div>
</div>
</div>
))}
</div>
)
}
Output:
Tailwind responsive grid: 1 col (mobile) → 2 cols (md) → 3 cols (lg). Cards with image, title, description, and CTA button
❓ FAQ
style tags into the DOM during rendering, resulting in a slight additional overhead during the first render. For most applications, the difference is negligible, but CSS Modules have an advantage in performance-sensitive animation scenarios.@apply directive to extract multiple utility classes into custom CSS classes, or use the clsx library for conditional concatenation. Additionally, most editors (such as VS Code with Tailwind CSS IntelliSense) offer class name autocompletion and hover previews, so class name readability isn’t an issue in actual development.$variant in styled-components have the $ prefix?$ prefix is part of the "transient prop" convention introduced in styled-components v5.2+, indicating that this prop is used only for style calculations and will not be passed down to the underlying DOM elements. Without the $ prefix, the prop will be rendered as an HTML attribute (such as <button variant="primary">), resulting in a console warning.<style> tags every time a component renders. However, the actual impact is usually negligible because: ① React 18’s concurrent rendering has already mitigated rendering blocking; ② most performance bottlenecks stem from JavaScript logic rather than style calculations; ③ if you do encounter performance issues, you can switch to a zero-runtime solution (such as vanilla-extract or Panda CSS).📖 Summary
- CSS Modules achieve style isolation through compile-time hashing; they are suitable for small and medium-sized projects and have zero runtime overhead.
- Tailwind CSS uses atomic class names to drive UI development, and when combined with PurgeCSS, the resulting code is extremely compact.
- styled-components provides props-driven dynamic styling and the ThemeProvider theme system, making it suitable for large-scale design systems
- The three approaches can be used in combination, allowing for flexible selection based on component type and project phase
- Regardless of which approach you choose, you must establish consistent style guidelines and conventions for the team.
📝 Exercises
- Create a
Avatarcomponent using CSS Modules: It supports two props,size(small/medium/large) andshape(circle/square), with the three sizes corresponding to different widths and heights. - Rewrite the Avatar component above using Tailwind CSS, keeping responsive design in mind: use
smallon mobile devices, and it will automatically switch tomediumon desktop. - Use styled-components to create a
Badgecomponent that controls the displayed number via thecountprop; if the number exceeds 99, display "99+"; and use thetheme.colorsprop to set different colors.