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)