Styling in React
CSS modules, styled-components, and Tailwind CSS
Styling Options in React
React doesn't dictate how you style your components. You have many options: plain CSS, CSS modules, CSS-in-JS libraries, and utility-first frameworks like Tailwind. Each has its trade-offs.
Inline Styles
Pass a JavaScript object to the style prop:
function InlineStyles() {
const buttonStyle = {
backgroundColor: 'blue',
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
};
// Dynamic styles
const [isActive, setIsActive] = useState(false);
return (
<div>
<button style={buttonStyle}>Static Style</button>
<button
style={{
...buttonStyle,
backgroundColor: isActive ? 'green' : 'blue'
}}
onClick={() => setIsActive(!isActive)}
>
Dynamic Style
</button>
</div>
);
}
⚠️ Inline Style Limitations
- • No pseudo-classes (:hover, :focus)
- • No media queries
- • No keyframe animations
- • camelCase properties (backgroundColor, not background-color)
Plain CSS
Import CSS files directly into your components:
/* Button.css */
.button {
background-color: blue;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
}
.button:hover {
background-color: darkblue;
}
.button.active {
background-color: green;
}
// Button.jsx
import './Button.css';
function Button({ isActive, children }) {
return (
<button className={`button ${isActive ? 'active' : ''}`}>
{children}
</button>
);
}
CSS Modules
Locally scoped CSS that avoids naming conflicts:
/* Button.module.css */
.button {
background-color: blue;
color: white;
padding: 10px 20px;
}
.button:hover {
background-color: darkblue;
}
.primary {
background-color: blue;
}
.secondary {
background-color: gray;
}
// Button.jsx
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
// Classes are transformed to unique names:
// .button_abc123 .primary_xyz789
Tailwind CSS
Utility-first CSS framework—extremely popular with React:
// No separate CSS file needed!
function Button({ variant = 'primary', children }) {
const baseClasses = 'px-4 py-2 rounded font-semibold transition-colors';
const variants = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600'
};
return (
<button className={`${baseClasses} ${variants[variant]}`}>
{children}
</button>
);
}
// Card component
function Card({ title, children }) {
return (
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
<h3 className="text-xl font-bold mb-4 text-gray-900">{title}</h3>
<div className="text-gray-600">{children}</div>
</div>
);
}
// Responsive design
function ResponsiveGrid() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card title="Card 1">Content</Card>
<Card title="Card 2">Content</Card>
<Card title="Card 3">Content</Card>
</div>
);
}
clsx / classnames
Utility for conditionally joining class names:
import clsx from 'clsx';
// or: import classNames from 'classnames';
function Button({ variant, size, disabled, className }) {
return (
<button
className={clsx(
// Base classes (always applied)
'font-semibold rounded transition-colors',
// Variant classes
{
'bg-blue-500 text-white': variant === 'primary',
'bg-gray-200 text-gray-800': variant === 'secondary',
'bg-red-500 text-white': variant === 'danger',
},
// Size classes
{
'px-2 py-1 text-sm': size === 'small',
'px-4 py-2': size === 'medium',
'px-6 py-3 text-lg': size === 'large',
},
// State classes
disabled && 'opacity-50 cursor-not-allowed',
// Custom classes from props
className
)}
disabled={disabled}
>
Click me
</button>
);
}
Styled Components
CSS-in-JS library for component-level styles:
import styled from 'styled-components';
// Create a styled button
const Button = styled.button`
background-color: ${props => props.primary ? 'blue' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: ${props => props.primary ? 'darkblue' : 'darkgray'};
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
// Extending styles
const PrimaryButton = styled(Button)`
background-color: blue;
font-weight: bold;
`;
// Using props for dynamic styles
const Card = styled.div`
background: white;
border-radius: 8px;
padding: ${props => props.padding || '16px'};
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;
function App() {
return (
<Card padding="24px">
<Button>Normal</Button>
<Button primary>Primary</Button>
<PrimaryButton>Extended</PrimaryButton>
</Card>
);
}
Comparison Table
| Approach | Pros | Cons |
|---|---|---|
| Inline Styles | Simple, dynamic, no files | No pseudo-classes, verbose |
| Plain CSS | Full CSS features, familiar | Global scope, naming conflicts |
| CSS Modules | Scoped, full CSS features | Extra files, build setup |
| Tailwind CSS | Fast, consistent, small bundle | Learning curve, long classes |
| Styled Components | Scoped, dynamic, full CSS | Runtime cost, extra dependency |
🎯 Styling Best Practices
- ✓ Choose one primary approach and be consistent
- ✓ Use CSS Modules or Tailwind for most projects
- ✓ Use clsx/classnames for conditional classes
- ✓ Keep styles close to components
- ✓ Use CSS variables for theming
- ✓ Avoid inline styles except for truly dynamic values
- ✓ Consider design system libraries (Chakra UI, Radix, shadcn/ui)
CSS Modules — The Safe Default
CSS Modules are built into every major React framework (Next.js, Vite, Create React App) with zero configuration. Class names are automatically scoped to the component, so .button in one file never clashes with .button in another. You get all of CSS — pseudo-classes, media queries, keyframes — without any runtime overhead or extra dependencies.
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.button:hover { opacity: 0.9; }
.button:disabled { opacity: 0.5; cursor: not-allowed; }
.primary { background: #2563eb; color: white; }
.secondary { background: #e5e7eb; color: #111; }
@media (max-width: 640px) {
.button { width: 100%; }
}
import styles from './Button.module.css';
import clsx from 'clsx'; // npm i clsx
function Button({ variant = 'primary', disabled, children, onClick }) {
return (
<button
className={clsx(styles.button, styles[variant], { [styles.disabled]: disabled })}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}