Styling overview
In this chapter, you get an overview of styling approaches in React and learn the characteristics of each approach and how to choose between them.
What you learn in this chapter
- The characteristics of the five styling approaches available in React
- The pros and cons of each approach
- How to choose a styling approach suited to your project
- Efficient class name management with
clsx
This chapter uses the project created in Chapter 3. Start the dev server with npm run dev and learn while writing code.
Styling approaches in React
React has multiple styling approaches, and you can choose one according to your project's requirements.
The main approaches
| Approach | Characteristic | Representative tools |
|---|---|---|
| Inline styles | Written directly in JSX | the style attribute |
| CSS files | Traditional CSS | .css files |
| CSS Modules | Scoped CSS | .module.css |
| CSS-in-JS | Styles defined in JS | styled-components, Emotion |
| Utility CSS | Combining classes | Tailwind CSS |
Inline styles
A method of passing an object to JSX's style attribute.
function InlineStyleExample() {
const buttonStyle = {
backgroundColor: '#3b82f6',
color: 'white',
padding: '8px 16px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer'
}
return (
<button style={buttonStyle}>
Click
</button>
)
}
Characteristics
- Pros: you can use JavaScript values directly; tightly coupled with the component
- Cons: pseudo-classes (such as :hover) cannot be used; performance concerns
// Dynamic styles
function DynamicStyle({ isActive }: { isActive: boolean }) {
return (
<div
style={{
backgroundColor: isActive ? '#22c55e' : '#ef4444',
transition: 'background-color 0.3s'
}}
>
{isActive ? 'Active' : 'Inactive'}
</div>
)
}
With inline styles, property names are in camelCase. background-color → backgroundColor
Try it: create a button with inline styles
In src/App.tsx, create a button with a blue background and white text using inline styles.
Hint
- Pass an object to the
styleattribute - Property names are in camelCase (
backgroundColor, etc.) - Substitute for hover effects with the
onMouseEnter/onMouseLeaveevents - Enable type completion with the
React.CSSPropertiestype
Answer and explanation
// src/App.tsx
function App() {
const buttonStyle: React.CSSProperties = {
backgroundColor: '#3b82f6',
color: 'white',
padding: '8px 16px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer'
}
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.style.backgroundColor = '#2563eb'
}
const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.style.backgroundColor = '#3b82f6'
}
return (
<button
style={buttonStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
Click
</button>
)
}
export default App
Because :hover cannot be used with inline styles, this substitutes with the onMouseEnter/onMouseLeave events. Specifying the React.CSSProperties type enables TypeScript completion.
CSS files (global CSS)
A method of importing traditional CSS files.
/* styles.css */
.button {
background-color: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.button:hover {
background-color: #2563eb;
}
.button--secondary {
background-color: #6b7280;
}
// Button.tsx
import './styles.css'
function Button({ variant = 'primary' }: { variant?: 'primary' | 'secondary' }) {
const className = variant === 'secondary'
? 'button button--secondary'
: 'button'
return <button className={className}>Button</button>
}
Characteristics
- Pros: familiar CSS, full features (media queries, pseudo-classes, etc.)
- Cons: global scope, possibility of name collisions
CSS Modules
A method of modularizing CSS files and automatically scoping class names.
/* Button.module.css */
.button {
background-color: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.button:hover {
background-color: #2563eb;
}
.secondary {
background-color: #6b7280;
}
.secondary:hover {
background-color: #4b5563;
}
// Button.tsx
import styles from './Button.module.css'
type ButtonProps = {
variant?: 'primary' | 'secondary'
children: React.ReactNode
}
function Button({ variant = 'primary', children }: ButtonProps) {
const className = variant === 'secondary'
? `${styles.button} ${styles.secondary}`
: styles.button
return <button className={className}>{children}</button>
}
Characteristics
- Pros: scoped (no name collisions), your existing CSS knowledge applies
- Cons: dynamic styling is cumbersome
The compiled class names
<!-- The built HTML -->
<button class="Button_button_x7ht2 Button_secondary_a3fk9">
Button
</button>
Try it: create a card with CSS Modules
Create src/components/Card.module.css and Card.tsx, and style a card component with CSS Modules.
Hint
- Create a
.module.cssfile - Import it with
import styles from './Card.module.css' - Use it as
className={styles.card} - Hover effects can also be written in CSS
Answer and explanation
/* src/components/Card.module.css */
.card {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: white;
}
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.content {
color: #666;
}
.card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
// src/components/Card.tsx
import styles from './Card.module.css'
type CardProps = {
title: string
children: React.ReactNode
}
function Card({ title, children }: CardProps) {
return (
<div className={styles.card}>
<h2 className={styles.title}>{title}</h2>
<div className={styles.content}>{children}</div>
</div>
)
}
export default Card
// Usage in src/App.tsx
import Card from './components/Card'
function App() {
return (
<Card title="Card title">
The card content goes here.
</Card>
)
}
CSS Modules automatically scopes class names with Vite by using the .module.css extension. You access them as object properties, like styles.card.
CSS-in-JS
An approach for defining styles in JavaScript. styled-components and Emotion are representative.
styled-components
import styled from 'styled-components'
const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>`
background-color: ${props =>
props.$variant === 'secondary' ? '#6b7280' : '#3b82f6'};
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
&:hover {
background-color: ${props =>
props.$variant === 'secondary' ? '#4b5563' : '#2563eb'};
}
`
// Usage
function App() {
return (
<>
<Button>Primary</Button>
<Button $variant="secondary">Secondary</Button>
</>
)
}
Emotion
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const buttonStyle = (variant: 'primary' | 'secondary') => css`
background-color: ${variant === 'secondary' ? '#6b7280' : '#3b82f6'};
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
&:hover {
background-color: ${variant === 'secondary' ? '#4b5563' : '#2563eb'};
}
`
function Button({ variant = 'primary' }: { variant?: 'primary' | 'secondary' }) {
return <button css={buttonStyle(variant)}>Button</button>
}
Characteristics
- Pros: dynamic styling is easy, scoped, tightly coupled with the component
- Cons: runtime overhead, increased bundle size, poor compatibility with React Server Components
Caveats about CSS-in-JS
- Compatibility issues with React Server Components: "runtime CSS-in-JS" such as styled-components and Emotion has poor compatibility with server-side rendering, and requires additional handling for use with the Next.js App Router. In 2022, the Emotion maintainer announced moving away from CSS-in-JS, and in March 2025, styled-components itself announced entering maintenance mode on OpenCollective, with the maintainer stating clearly that "adoption in new projects is not recommended." The React team has also pointed out the compatibility issue between runtime CSS-in-JS and RSC.
- Runtime cost: because the style string is generated on every render, the performance impact is not negligible. It is a cost that even the React Compiler's automatic memoization cannot avoid.
- Recommended alternatives: for new projects, Tailwind CSS, CSS Modules, or CSS-in-JS that generates static CSS at build time (vanilla-extract, Panda CSS, Linaria, etc.) are recommended.
- Existing projects: if you already use styled-components / Emotion and performance is not a problem, there is no need to migrate forcibly. However, you should consider another option at the point you adopt React Server Components.
Utility CSS (Tailwind CSS)
An approach to styling by combining predefined utility classes.
function Button({ variant = 'primary' }: { variant?: 'primary' | 'secondary' }) {
const baseClasses = 'px-4 py-2 rounded-sm border-none cursor-pointer transition-colors'
const variantClasses = variant === 'secondary'
? 'bg-gray-500 text-white hover:bg-gray-600'
: 'bg-blue-500 text-white hover:bg-blue-600'
return (
<button className={`${baseClasses} ${variantClasses}`}>
Button
</button>
)
}
Characteristics
- Pros: fast development, a consistent design system, no CSS files needed
- Cons: class names get long, a learning cost
Tailwind CSS is explained in detail in the next chapter.
Comparison of styling approaches
Performance
| Approach | Runtime | Bundle size |
|---|---|---|
| CSS Modules | None | Small |
| Tailwind CSS | None | Small (only the used classes are generated) |
| styled-components | Yes | Large |
| Inline styles | Yes | Medium |
Developer experience
| Approach | TypeScript support | Dynamic styles | Editor completion |
|---|---|---|---|
| CSS Modules | △ | △ | ○ |
| Tailwind CSS | ○ | ○ | ◎ |
| CSS-in-JS | ◎ | ◎ | ◎ |
| Inline styles | ◎ | ◎ | ○ |
Selection guidelines
Tailwind CSS is recommended when
- You need rapid UI development
- You value the consistency of a design system
- You value performance
- It is the main recommendation of this book
CSS Modules is recommended when
- You want to leverage your existing CSS skills
- It is a simple project
- You value framework independence
CSS-in-JS is recommended when
- You need advanced dynamic styling
- You are developing a component library
- Theme switching is frequent
Utilities for combining className
When you want to conditionally combine multiple class names, you can use a utility library.
clsx
npm install clsx
import clsx from 'clsx'
type ButtonProps = {
variant?: 'primary' | 'secondary'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}
function Button({ variant = 'primary', size = 'md', disabled }: ButtonProps) {
return (
<button
className={clsx(
// Base classes
'rounded-sm font-medium transition-colors',
// Size
{
'px-2 py-1 text-sm': size === 'sm',
'px-4 py-2': size === 'md',
'px-6 py-3 text-lg': size === 'lg'
},
// Variant
{
'bg-blue-500 text-white hover:bg-blue-600': variant === 'primary',
'bg-gray-500 text-white hover:bg-gray-600': variant === 'secondary'
},
// State
disabled && 'opacity-50 cursor-not-allowed'
)}
disabled={disabled}
>
Button
</button>
)
}
tailwind-merge (Tailwind CSS only)
Resolves class conflicts in Tailwind CSS.
npm install tailwind-merge
import { twMerge } from 'tailwind-merge'
// The later class takes precedence
twMerge('px-2 py-1', 'px-4')
// => 'py-1 px-4'
// Use it in combination with clsx
import clsx from 'clsx'
function cn(...inputs: Parameters<typeof clsx>) {
return twMerge(clsx(inputs))
}
Try it: introduce clsx
Install it with npm install clsx and try combining class names conditionally. Create a button component that supports variants (primary/secondary/danger) and sizes (sm/md/lg).
Hint
- Install it with
npm install clsx - Import it with
import clsx from 'clsx' - Combine conditionally with
clsx('class1', condition && 'class2') - The object form
{ 'class': condition }is also usable
Answer and explanation
npm install clsx
// src/components/Button.tsx
import clsx from 'clsx'
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
children: React.ReactNode
}
function Button({
variant = 'primary',
size = 'md',
disabled = false,
children
}: ButtonProps) {
// An example combined with Tailwind CSS
return (
<button
className={clsx(
'rounded-sm font-bold transition-colors',
{
'px-2 py-1 text-xs': size === 'sm',
'px-4 py-2 text-sm': size === 'md',
'px-6 py-3 text-base': size === 'lg'
},
{
'bg-blue-500 text-white hover:bg-blue-600': variant === 'primary',
'bg-gray-500 text-white hover:bg-gray-600': variant === 'secondary',
'bg-red-500 text-white hover:bg-red-600': variant === 'danger'
},
disabled && 'opacity-50 cursor-not-allowed'
)}
disabled={disabled}
>
{children}
</button>
)
}
export default Button
clsx is a utility for combining class names conditionally. You can write a mix of strings, objects, and arrays, and falsy values are automatically excluded. It has especially good compatibility with Tailwind CSS, and complex variant management can be written concisely.
Summary
- React has multiple styling approaches; choose one according to your project
- Inline styles: dynamic but with feature limitations
- CSS Modules: safe with scoping
- CSS-in-JS: flexible but with a runtime cost
- Tailwind CSS: fast development, good performance (recommended in this book)
- Make class name management more efficient with
clsxandtailwind-merge
In the next chapter, you will learn in detail about Tailwind CSS, which this book adopts.