π¨ Styling in React
You've built functional components - now let's make them beautiful! React offers many ways to style your components, from traditional CSS files to modern CSS-in-JS solutions. Each approach has its strengths, and choosing the right one can make your development faster and your apps more maintainable. In this lesson, we'll explore all the major styling approaches so you can pick the best tool for each job. Let's paint this canvas! ποΈ
π― Learning Objectives
By the end of this lesson, you will be able to:
- Apply traditional CSS stylesheets to React components
- Use CSS Modules for scoped, component-specific styles
- Write inline styles with JavaScript objects
- Implement dynamic and conditional styling
- Understand CSS-in-JS solutions and their benefits
- Use Tailwind CSS for utility-first styling
- Choose the right styling approach for your project
- Apply responsive design principles in React
Estimated Time: 70-85 minutes
Project: Style a complete dashboard interface using multiple styling techniques
π In This Lesson
πΊοΈ Styling Approaches Overview
React doesn't enforce a specific styling approach - you have options! Let's understand the landscape before diving into each method.
π Key Concept
Unlike traditional HTML where styles are separate, React components can bundle their styles with their logic. This "component-based styling" is a paradigm shift that offers new possibilities for organization and maintainability.
The Styling Spectrum
React styling approaches range from traditional to cutting-edge:
| Approach | Learning Curve | Performance | Best For |
|---|---|---|---|
| Plain CSS | Easy (you know it!) | Excellent | Simple projects, global styles |
| CSS Modules | Easy | Excellent | Component-scoped styles |
| Inline Styles | Easy | Good | Dynamic styles, simple components |
| Styled Components | Moderate | Good | CSS-in-JS, dynamic theming |
| Tailwind CSS | Moderate | Excellent | Rapid development, utility-first |
| Emotion/JSS | Moderate-Hard | Good | Advanced CSS-in-JS needs |
What We'll Cover
This lesson focuses on the most practical, widely-used approaches:
- Traditional CSS - The foundation everyone knows
- CSS Modules - Scoped styles without conflicts
- Inline Styles - JavaScript-powered dynamic styling
- CSS-in-JS - Modern component-centric styling
- Tailwind CSS - Utility-first rapid development
π‘ No "Best" Approach
There's no single "best" way to style React apps. Each approach has trade-offs. Many professional apps use multiple approaches - global CSS for resets, CSS Modules for components, and inline styles for dynamic values. Choose based on your project needs!
π¨ Interactive: Compare Styling Approaches
Click each approach to see how the same button would be coded:
π Traditional CSS Stylesheets
Let's start with what you already know - regular CSS files! This is the simplest approach and works great for many projects.
Basic CSS Import
You can import CSS files directly into your components:
// Button.tsx
import './Button.css';
function Button({ text, onClick }: ButtonProps) {
return (
<button className="btn" onClick={onClick}>
{text}
</button>
);
}
export default Button;
/* Button.css */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background-color: #667eea;
color: white;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #5568d3;
}
.btn:active {
background-color: #4451b8;
}
.btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
Global vs Component Styles
Structure your CSS for maintainability:
src/
βββ index.css # Global styles, resets
βββ App.tsx
βββ App.css # App-level styles
βββ components/
βββ Button/
β βββ Button.tsx
β βββ Button.css # Button-specific styles
βββ Card/
βββ Card.tsx
βββ Card.css # Card-specific styles
Class Name Composition
Build class names dynamically:
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
}
function Button({
text,
variant = 'primary',
size = 'medium',
disabled = false,
onClick
}: ButtonProps) {
// Method 1: Template literal
const className = `btn btn-${variant} btn-${size}`;
// Method 2: Array join
const classes = ['btn', `btn-${variant}`, `btn-${size}`];
if (disabled) classes.push('btn-disabled');
const classNameAlt = classes.join(' ');
return (
<button
className={className}
disabled={disabled}
onClick={onClick}
>
{text}
</button>
);
}
/* Button variants */
.btn-primary {
background-color: #667eea;
}
.btn-secondary {
background-color: #6c757d;
}
.btn-danger {
background-color: #dc3545;
}
/* Button sizes */
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.btn-medium {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn-large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
Using the classnames Library
For complex class logic, use the popular classnames (or clsx) library:
# Install
npm install classnames
npm install --save-dev @types/classnames
import classNames from 'classnames';
import './Button.css';
function Button({
text,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick
}: ButtonProps) {
const buttonClasses = classNames('btn', {
[`btn-${variant}`]: variant,
[`btn-${size}`]: size,
'btn-disabled': disabled,
'btn-loading': loading,
});
return (
<button
className={buttonClasses}
disabled={disabled || loading}
onClick={onClick}
>
{loading ? 'Loading...' : text}
</button>
);
}
β Pros of Traditional CSS
- Familiar - no new syntax to learn
- Works with existing CSS knowledge
- Great tooling support (IDE autocomplete, linters)
- Easy to share styles across components
- Excellent performance
β οΈ Cons of Traditional CSS
- Global namespace - class names can conflict
- No automatic scoping to components
- Dead code can accumulate (unused styles)
- Need to manage class name uniqueness manually
BEM Naming Convention
Use BEM (Block Element Modifier) to avoid conflicts:
/* Block */
.card { }
/* Elements */
.card__header { }
.card__body { }
.card__footer { }
/* Modifiers */
.card--primary { }
.card--large { }
.card__header--highlighted { }
function Card({ variant = 'default', size = 'medium' }: CardProps) {
return (
<div className={`card card--${variant} card--${size}`}>
<div className="card__header">
<h2>Card Title</h2>
</div>
<div className="card__body">
<p>Card content</p>
</div>
<div className="card__footer">
<button>Action</button>
</div>
</div>
);
}
π CSS Modules
CSS Modules solve the global namespace problem by automatically scoping styles to components. They're like regular CSS but with superpowers!
π Definition
CSS Modules: A CSS file where all class names are scoped locally by default. At build time, class names are automatically made unique, preventing conflicts.
Creating a CSS Module
Name your CSS file with the .module.css extension:
Button.tsx
Button.module.css β Notice the .module.css extension
/* Button.module.css */
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.primary {
background-color: #667eea;
color: white;
}
.secondary {
background-color: #6c757d;
color: white;
}
.large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
Using CSS Modules
Import the CSS Module as an object:
// Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
text: string;
variant?: 'primary' | 'secondary';
size?: 'medium' | 'large';
onClick?: () => void;
}
function Button({
text,
variant = 'primary',
size = 'medium',
onClick
}: ButtonProps) {
// Access classes as object properties
return (
<button
className={`${styles.button} ${styles[variant]} ${styles[size]}`}
onClick={onClick}
>
{text}
</button>
);
}
export default Button;
How CSS Modules Work
At build time, class names are transformed:
/* You write: */
.button { }
/* Browser sees: */
.Button_button__2Xk3q { }
The generated class name is unique, preventing conflicts!
π¨ Interactive: CSS Module Scoping
See how CSS Modules transform your class names to prevent conflicts:
Composition in CSS Modules
CSS Modules support composition - reusing styles:
/* Button.module.css */
.base {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.primary {
composes: base;
background-color: #667eea;
color: white;
}
.secondary {
composes: base;
background-color: #6c757d;
color: white;
}
/* Can even compose from other files */
.special {
composes: primary;
composes: animated from './animations.module.css';
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
// Simpler usage - no need to combine classes!
function Button({ variant = 'primary' }: ButtonProps) {
return (
<button className={styles[variant]}>
Click me
</button>
);
}
Global Styles in CSS Modules
Sometimes you need global classes:
/* Button.module.css */
/* Local by default */
.button {
padding: 0.5rem 1rem;
}
/* Explicitly global */
:global(.btn-reset) {
all: unset;
}
/* Mix local and global */
.button :global(.icon) {
margin-right: 0.5rem;
}
Using with classnames
Combine CSS Modules with the classnames library:
import classNames from 'classnames';
import styles from './Button.module.css';
function Button({
variant = 'primary',
size = 'medium',
disabled = false
}: ButtonProps) {
const buttonClasses = classNames(
styles.button,
styles[variant],
styles[size],
{
[styles.disabled]: disabled
}
);
return (
<button className={buttonClasses}>
Click me
</button>
);
}
β Pros of CSS Modules
- Automatic scoping - no naming conflicts
- Still write regular CSS
- No runtime cost - processed at build time
- Works with existing tools
- Composition feature is powerful
- Easy to understand what styles apply where
β οΈ Things to Watch
- Syntax is slightly different from plain CSS
- Need to use bracket notation for dynamic classes:
styles[variant] - Can't use kebab-case class names without brackets
- Harder to override styles from parent components
TypeScript Support
Get type safety for CSS Modules:
# Install typescript plugin
npm install --save-dev typescript-plugin-css-modules
// tsconfig.json
{
"compilerOptions": {
"plugins": [{ "name": "typescript-plugin-css-modules" }]
}
}
Now you get autocomplete for class names!
import styles from './Button.module.css';
// TypeScript knows what classes exist!
styles.button // β
Autocomplete works
styles.primary // β
Autocomplete works
styles.invalid // β TypeScript error!
βοΈ Inline Styles
React lets you write styles directly in JavaScript using the style prop. This is perfect for dynamic styling based on props or state!
Basic Inline Styles
Pass a JavaScript object to the style prop:
function Box() {
return (
<div style={{
backgroundColor: '#667eea',
color: 'white',
padding: '1rem',
borderRadius: '8px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
Styled Box
</div>
);
}
β οΈ Remember: camelCase Properties
CSS properties become camelCase in JavaScript:
background-colorβbackgroundColorfont-sizeβfontSizemargin-topβmarginTop
Dynamic Inline Styles
The real power of inline styles is dynamic values:
interface ProgressBarProps {
percentage: number;
color?: string;
height?: number;
}
function ProgressBar({
percentage,
color = '#667eea',
height = 20
}: ProgressBarProps) {
const containerStyle = {
width: '100%',
height: `${height}px`,
backgroundColor: '#e0e0e0',
borderRadius: '10px',
overflow: 'hidden'
};
const fillStyle = {
width: `${percentage}%`,
height: '100%',
backgroundColor: color,
transition: 'width 0.3s ease'
};
return (
<div style={containerStyle}>
<div style={fillStyle} />
</div>
);
}
// Usage with dynamic values
<ProgressBar percentage={75} color="#4CAF50" height={30} />
Conditional Inline Styles
Change styles based on conditions:
interface MessageProps {
type: 'info' | 'success' | 'warning' | 'error';
text: string;
}
function Message({ type, text }: MessageProps) {
const getBackgroundColor = () => {
switch (type) {
case 'info': return '#2196F3';
case 'success': return '#4CAF50';
case 'warning': return '#FFC107';
case 'error': return '#F44336';
default: return '#666';
}
};
const messageStyle = {
padding: '1rem',
borderRadius: '4px',
backgroundColor: getBackgroundColor(),
color: 'white',
marginBottom: '1rem'
};
return <div style={messageStyle}>{text}</div>;
}
Extracting Style Objects
For cleaner code, extract style objects:
// Define styles outside component (won't recreate on every render)
const styles = {
container: {
display: 'flex',
flexDirection: 'column' as const,
gap: '1rem',
padding: '2rem'
},
header: {
fontSize: '2rem',
fontWeight: 'bold',
color: '#333'
},
content: {
fontSize: '1rem',
lineHeight: 1.6,
color: '#666'
}
};
function Article() {
return (
<div style={styles.container}>
<h1 style={styles.header}>Article Title</h1>
<p style={styles.content}>Article content...</p>
</div>
);
}
Merging Style Objects
Combine multiple style objects:
const baseButtonStyle = {
padding: '0.5rem 1rem',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
};
const primaryButtonStyle = {
...baseButtonStyle,
backgroundColor: '#667eea',
color: 'white'
};
const disabledButtonStyle = {
...baseButtonStyle,
backgroundColor: '#ccc',
cursor: 'not-allowed'
};
function Button({ disabled }: { disabled: boolean }) {
const buttonStyle = disabled ? disabledButtonStyle : primaryButtonStyle;
return <button style={buttonStyle}>Click me</button>;
}
β Pros of Inline Styles
- Dynamic values are easy - just use JavaScript
- No naming conflicts - styles are scoped to element
- Full power of JavaScript for calculations
- Easy to see what styles apply to an element
- Props flow directly to styles
β οΈ Cons of Inline Styles
- No pseudo-classes (:hover, :focus, etc.)
- No media queries for responsive design
- No CSS animations (only transitions)
- Verbose for complex styles
- Slight performance impact (vs static CSS)
- No autoprefixing for browser compatibility
When to Use Inline Styles
Inline styles work best for:
- Dynamic values calculated from props/state
- Values that change frequently
- Component-specific styles that won't be reused
- Quick prototyping
Avoid inline styles for:
- Static styles that don't change
- Complex layouts requiring media queries
- Styles needing pseudo-classes
- Reusable design systems
π Dynamic and Conditional Styling
React's real power comes from making styles react to your data. Let's master dynamic styling techniques!
Conditional Classes
Apply classes based on conditions:
interface ButtonProps {
text: string;
variant: 'primary' | 'secondary';
isActive: boolean;
isLoading: boolean;
}
function Button({ text, variant, isActive, isLoading }: ButtonProps) {
// Method 1: Template literal
const className = `btn btn-${variant} ${isActive ? 'active' : ''} ${isLoading ? 'loading' : ''}`;
// Method 2: Array filter join
const classNameAlt = [
'btn',
`btn-${variant}`,
isActive && 'active',
isLoading && 'loading'
].filter(Boolean).join(' ');
// Method 3: classnames library (recommended)
const classNameBest = classNames('btn', `btn-${variant}`, {
active: isActive,
loading: isLoading
});
return <button className={classNameBest}>{text}</button>;
}
Style Based on Props
Calculate styles from prop values:
interface AvatarProps {
name: string;
size?: number;
color?: string;
imageUrl?: string;
}
function Avatar({ name, size = 40, color, imageUrl }: AvatarProps) {
// Generate color from name if not provided
const getColorFromName = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = hash % 360;
return `hsl(${hue}, 70%, 60%)`;
};
const backgroundColor = color || getColorFromName(name);
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase();
const avatarStyle = {
width: size,
height: size,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: size * 0.4,
fontWeight: 'bold',
color: 'white',
backgroundColor: backgroundColor,
backgroundImage: imageUrl ? `url(${imageUrl})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
};
return (
<div style={avatarStyle}>
{!imageUrl && initials}
</div>
);
}
// Usage
<Avatar name="Alice Johnson" size={60} />
<Avatar name="Bob Smith" size={40} color="#FF6B6B" />
<Avatar name="Charlie Brown" imageUrl="/charlie.jpg" />
Theme-Based Styling
Create a theme system:
// Theme definition
const theme = {
colors: {
primary: '#667eea',
secondary: '#6c757d',
success: '#4CAF50',
danger: '#F44336',
text: '#333',
textLight: '#666',
background: '#fff',
border: '#e0e0e0'
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem'
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
full: '9999px'
}
};
// Use theme in component
interface CardProps {
children: React.ReactNode;
variant?: 'default' | 'primary' | 'success';
}
function Card({ children, variant = 'default' }: CardProps) {
const getBackgroundColor = () => {
switch (variant) {
case 'primary': return theme.colors.primary;
case 'success': return theme.colors.success;
default: return theme.colors.background;
}
};
const cardStyle = {
padding: theme.spacing.lg,
borderRadius: theme.borderRadius.md,
backgroundColor: getBackgroundColor(),
border: `1px solid ${theme.colors.border}`,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
};
return <div style={cardStyle}>{children}</div>;
}
Animation with State
Animate components by changing styles:
import { useState } from 'react';
function AnimatedBox() {
const [isExpanded, setIsExpanded] = useState(false);
const boxStyle = {
width: isExpanded ? '200px' : '100px',
height: isExpanded ? '200px' : '100px',
backgroundColor: isExpanded ? '#4CAF50' : '#667eea',
borderRadius: isExpanded ? '50%' : '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
cursor: 'pointer',
transition: 'all 0.3s ease',
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'
};
return (
<div
style={boxStyle}
onClick={() => setIsExpanded(!isExpanded)}
>
Click me!
</div>
);
}
π¨ Interactive: Dynamic Styling Playground
Adjust the controls to see how inline styles update in real-time:
Responsive Values
Adjust styles based on screen size:
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
function ResponsiveGrid() {
const width = useWindowWidth();
const getColumns = () => {
if (width < 768) return 1;
if (width < 1024) return 2;
return 3;
};
const gridStyle = {
display: 'grid',
gridTemplateColumns: `repeat(${getColumns()}, 1fr)`,
gap: '1rem'
};
return (
<div style={gridStyle}>
{/* Grid items */}
</div>
);
}
π‘ Pro Tip: Combine Approaches
Use CSS/CSS Modules for static styles and inline styles for dynamic values:
function ProgressBar({ percentage }: { percentage: number }) {
return (
<div className="progress-container">
<div
className="progress-fill"
style={{ width: `${percentage}%` }} // Dynamic value
/>
</div>
);
}
π CSS-in-JS Solutions
CSS-in-JS libraries let you write CSS directly in your JavaScript with the full power of both languages. The most popular is styled-components!
Introduction to styled-components
styled-components lets you create components with built-in styles:
# Install styled-components
npm install styled-components
npm install --save-dev @types/styled-components
import styled from 'styled-components';
// Create a styled component
const Button = styled.button`
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background-color: #667eea;
color: white;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #5568d3;
}
&:active {
background-color: #4451b8;
}
`;
// Use it like a regular component
function App() {
return <Button>Click me</Button>;
}
Props in styled-components
styled-components can use props to change styles:
interface ButtonProps {
$variant?: 'primary' | 'secondary' | 'danger';
$size?: 'small' | 'medium' | 'large';
}
const Button = styled.button<ButtonProps>`
padding: ${props => {
switch (props.$size) {
case 'small': return '0.25rem 0.5rem';
case 'large': return '0.75rem 1.5rem';
default: return '0.5rem 1rem';
}
}};
font-size: ${props => {
switch (props.$size) {
case 'small': return '0.875rem';
case 'large': return '1.125rem';
default: return '1rem';
}
}};
background-color: ${props => {
switch (props.$variant) {
case 'secondary': return '#6c757d';
case 'danger': return '#F44336';
default: return '#667eea';
}
}};
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
opacity: 0.9;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`;
// Usage
<Button $variant="primary" $size="large">Large Primary</Button>
<Button $variant="danger" $size="small">Small Danger</Button>
β οΈ Transient Props
In styled-components v5.1+, prefix props meant only for styling with $ to prevent them from being passed to the DOM. This avoids React warnings about invalid HTML attributes.
Extending Styles
Create variations by extending styled components:
const Button = styled.button`
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
`;
const PrimaryButton = styled(Button)`
background-color: #667eea;
color: white;
`;
const OutlineButton = styled(Button)`
background-color: transparent;
color: #667eea;
border: 2px solid #667eea;
`;
const IconButton = styled(Button)`
padding: 0.5rem;
width: 40px;
height: 40px;
border-radius: 50%;
`;
Theming with styled-components
Create a theme provider for global styles:
import { ThemeProvider } from 'styled-components';
const theme = {
colors: {
primary: '#667eea',
secondary: '#6c757d',
text: '#333',
background: '#fff'
},
spacing: {
sm: '0.5rem',
md: '1rem',
lg: '1.5rem'
}
};
// Components can access theme
const Container = styled.div`
padding: ${props => props.theme.spacing.lg};
background-color: ${props => props.theme.colors.background};
color: ${props => props.theme.colors.text};
`;
const Title = styled.h1`
color: ${props => props.theme.colors.primary};
margin-bottom: ${props => props.theme.spacing.md};
`;
// Wrap your app
function App() {
return (
<ThemeProvider theme={theme}>
<Container>
<Title>Themed App</Title>
</Container>
</ThemeProvider>
);
}
Global Styles
Define global styles with createGlobalStyle:
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu;
line-height: 1.6;
color: #333;
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 1rem;
}
`;
function App() {
return (
<>
<GlobalStyle />
{/* Your app components */}
</>
);
}
β Pros of CSS-in-JS
- Scoped styles automatically
- Full power of JavaScript in styles
- Dynamic theming is easy
- Dead code elimination (unused styles removed)
- Better TypeScript integration
- Can use props directly in styles
β οΈ Cons of CSS-in-JS
- Learning curve - new syntax to learn
- Runtime overhead (styles generated in browser)
- Larger bundle size
- Can't use traditional CSS tools
- Server-side rendering requires extra setup
Other CSS-in-JS Libraries
Alternatives to styled-components:
- Emotion - Similar to styled-components, slightly faster
- JSS - More flexible, used by Material-UI
- Linaria - Zero-runtime CSS-in-JS
- Vanilla Extract - Type-safe, zero-runtime
π¬οΈ Tailwind CSS
Tailwind is a utility-first CSS framework that lets you build designs rapidly by composing utility classes. It's become incredibly popular in the React ecosystem!
What is Utility-First CSS?
Instead of writing custom CSS, you use pre-made utility classes:
// Traditional approach
<button className="custom-button">Click me</button>
/* custom-button {
padding: 0.5rem 1rem;
background-color: blue;
color: white;
border-radius: 0.25rem;
} */
// Tailwind approach
<button className="px-4 py-2 bg-blue-500 text-white rounded">
Click me
</button>
Installing Tailwind
Set up Tailwind in your project:
# Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Common Tailwind Utilities
Here are the most frequently used classes:
| Category | Example Classes | What They Do |
|---|---|---|
| Spacing | p-4, m-2, px-6, mt-8 | Padding and margins |
| Colors | bg-blue-500, text-red-600 | Background and text colors |
| Typography | text-xl, font-bold | Font size and weight |
| Layout | flex, grid, block, hidden | Display properties |
| Sizing | w-full, h-screen, max-w-lg | Width and height |
| Borders | border, rounded-lg | Border and border-radius |
Building Components with Tailwind
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
}
function Button({
children,
variant = 'primary',
size = 'md',
onClick
}: ButtonProps) {
const baseClasses = 'font-semibold rounded transition-colors duration-200';
const variantClasses = {
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
danger: 'bg-red-500 hover:bg-red-600 text-white'
};
const sizeClasses = {
sm: 'px-3 py-1 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
};
const className = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`;
return (
<button className={className} onClick={onClick}>
{children}
</button>
);
}
// Usage
<Button variant="primary" size="lg">Large Primary</Button>
<Button variant="danger" size="sm">Small Danger</Button>
Responsive Design with Tailwind
Tailwind makes responsive design incredibly easy:
function ResponsiveCard() {
return (
<div className="
p-4
md:p-6
lg:p-8
bg-white
rounded-lg
shadow-md
w-full
md:w-1/2
lg:w-1/3
">
<h2 className="text-xl md:text-2xl lg:text-3xl font-bold mb-4">
Responsive Card
</h2>
<p className="text-sm md:text-base text-gray-600">
This card adapts to different screen sizes.
</p>
</div>
);
}
// Breakpoints:
// sm: 640px
// md: 768px
// lg: 1024px
// xl: 1280px
// 2xl: 1536px
Custom Tailwind Classes
Use @apply to create custom component classes:
/* index.css */
@layer components {
.btn-primary {
@apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors;
}
.card {
@apply p-6 bg-white rounded-lg shadow-md;
}
.input-field {
@apply w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500;
}
}
// Use custom classes
function Form() {
return (
<div className="card">
<input type="text" className="input-field" />
<button className="btn-primary">Submit</button>
</div>
);
}
β Pros of Tailwind
- Extremely fast development
- No need to name things
- Consistent design system out of the box
- Responsive design is trivial
- Small final bundle (unused classes removed)
- Great documentation and community
β οΈ Cons of Tailwind
- Long className strings can be ugly
- Learning curve for utility names
- HTML gets more cluttered
- Harder to make global style changes
- Not everyone likes the approach
π± Responsive Design in React
Making your React apps look great on all screen sizes is crucial. Let's explore responsive design techniques!
Media Queries in CSS
Traditional media queries work perfectly in React:
/* Component.module.css */
.container {
padding: 1rem;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
/* Tablet and up */
@media (min-width: 768px) {
.container {
padding: 2rem;
}
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* Desktop and up */
@media (min-width: 1024px) {
.container {
padding: 3rem;
}
.grid {
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
}
JavaScript-Based Responsive Design
Use JavaScript to detect screen size and adjust components:
import { useState, useEffect } from 'react';
// Custom hook for window width
function useWindowWidth() {
const [width, setWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 0
);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}
// Custom hook for breakpoints
function useBreakpoint() {
const width = useWindowWidth();
return {
isMobile: width < 768,
isTablet: width >= 768 && width < 1024,
isDesktop: width >= 1024,
width
};
}
// Usage in component
function ResponsiveNav() {
const { isMobile, isDesktop } = useBreakpoint();
return (
<nav>
{isMobile && <MobileMenu />}
{isDesktop && <DesktopMenu />}
</nav>
);
}
Container Queries (Modern Approach)
React components can adapt to their container size:
/* Modern CSS Container Queries */
.card-container {
container-type: inline-size;
}
.card {
padding: 1rem;
}
.card-title {
font-size: 1rem;
}
/* When container is at least 400px wide */
@container (min-width: 400px) {
.card {
padding: 2rem;
}
.card-title {
font-size: 1.5rem;
}
}
Mobile-First vs Desktop-First
Choose your approach wisely:
/* Mobile-First (Recommended) */
/* Base styles for mobile */
.container {
padding: 1rem;
font-size: 14px;
}
/* Enhanced for larger screens */
@media (min-width: 768px) {
.container {
padding: 2rem;
font-size: 16px;
}
}
/* Desktop-First (Alternative) */
/* Base styles for desktop */
.container {
padding: 3rem;
font-size: 16px;
}
/* Simplified for smaller screens */
@media (max-width: 767px) {
.container {
padding: 1rem;
font-size: 14px;
}
}
β Mobile-First Benefits
- Smaller base bundle size
- Progressive enhancement mindset
- Better performance on mobile
- Simpler to add features for larger screens
π¨ Interactive: Responsive Breakpoints
Drag the slider to see how layouts change at different screen widths:
Responsive Images
Optimize images for different screen sizes:
function ResponsiveImage({
src,
alt,
sizes
}: {
src: string;
alt: string;
sizes?: string;
}) {
return (
<picture>
<source
media="(min-width: 1024px)"
srcSet={`${src}-large.jpg`}
/>
<source
media="(min-width: 768px)"
srcSet={`${src}-medium.jpg`}
/>
<img
src={`${src}-small.jpg`}
alt={alt}
loading="lazy"
style={{ width: '100%', height: 'auto' }}
/>
</picture>
);
}
Viewport Units
Use viewport-based units for fluid sizing:
.hero {
/* 100% of viewport height */
height: 100vh;
/* 50% of viewport width */
width: 50vw;
/* Minimum of 2rem or 5% of viewport width */
padding: max(2rem, 5vw);
/* Fluid font size */
font-size: clamp(1rem, 2.5vw, 3rem);
}
ποΈ Hands-on Practice
ποΈ Exercise 1: Style a Dashboard Card
Objective: Create a styled dashboard card using CSS Modules with responsive design.
Requirements:
- Create a Card component with CSS Module styling
- Include header, body, and footer sections
- Add hover effects and transitions
- Make it responsive (stack on mobile, side-by-side on desktop)
- Support light and dark variants
Starter Code:
// DashboardCard.tsx
import styles from './DashboardCard.module.css';
interface DashboardCardProps {
title: string;
value: string | number;
change: number;
variant?: 'light' | 'dark';
}
function DashboardCard({ title, value, change, variant = 'light' }: DashboardCardProps) {
// TODO: Implement component
return <div></div>;
}
export default DashboardCard;
π‘ Hint
Use CSS Grid or Flexbox for layout. Add a className that combines the base style with the variant. Use CSS transitions for smooth hover effects.
β Solution
// DashboardCard.tsx
import styles from './DashboardCard.module.css';
interface DashboardCardProps {
title: string;
value: string | number;
change: number;
variant?: 'light' | 'dark';
}
function DashboardCard({
title,
value,
change,
variant = 'light'
}: DashboardCardProps) {
const isPositive = change >= 0;
const cardClass = `${styles.card} ${styles[variant]}`;
const changeClass = isPositive ? styles.positive : styles.negative;
return (
<div className={cardClass}>
<div className={styles.header}>
<h3 className={styles.title}>{title}</h3>
</div>
<div className={styles.body}>
<div className={styles.value}>{value}</div>
<div className={`${styles.change} ${changeClass}`}>
{isPositive ? 'β' : 'β'} {Math.abs(change)}%
</div>
</div>
</div>
);
}
export default DashboardCard;
/* DashboardCard.module.css */
.card {
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.light {
background-color: white;
color: #333;
}
.dark {
background-color: #2d3748;
color: white;
}
.header {
margin-bottom: 1rem;
}
.title {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.7;
}
.body {
display: flex;
align-items: baseline;
gap: 1rem;
}
.value {
font-size: 2rem;
font-weight: bold;
}
.change {
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.positive {
color: #10b981;
background-color: #d1fae5;
}
.negative {
color: #ef4444;
background-color: #fee2e2;
}
/* Responsive */
@media (max-width: 768px) {
.card {
padding: 1rem;
}
.value {
font-size: 1.5rem;
}
}
ποΈ Exercise 2: Build a Theme Switcher
Objective: Create a theme switching system with light and dark modes.
Requirements:
- Create a theme context with light/dark modes
- Build a toggle button to switch themes
- Style components differently based on theme
- Persist theme choice in localStorage
β Solution
// ThemeContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(() => {
const saved = localStorage.getItem('theme');
return (saved as Theme) || 'light';
});
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// ThemeToggle.tsx
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="theme-toggle"
aria-label="Toggle theme"
>
{theme === 'light' ? 'π' : 'βοΈ'}
</button>
);
}
/* Global theme styles */
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f7fafc;
--text-primary: #1a202c;
--text-secondary: #4a5568;
--border-color: #e2e8f0;
}
:root[data-theme="dark"] {
--bg-primary: #1a202c;
--bg-secondary: #2d3748;
--text-primary: #f7fafc;
--text-secondary: #cbd5e0;
--border-color: #4a5568;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s, color 0.3s;
}
.theme-toggle {
padding: 0.5rem;
border: none;
background: var(--bg-secondary);
border-radius: 50%;
cursor: pointer;
font-size: 1.5rem;
transition: transform 0.2s;
}
.theme-toggle:hover {
transform: scale(1.1);
}
π― Quick Quiz
Question 1: What's the main benefit of CSS Modules?
Question 2: When should you use inline styles in React?
Question 3: What is Tailwind CSS?
π― Choosing the Right Approach
With so many options, how do you choose? Here's a practical guide based on different scenarios.
Decision Matrix
| Scenario | Recommended Approach | Why? |
|---|---|---|
| Small project, learning | Plain CSS or CSS Modules | Simple, no extra dependencies |
| Rapid prototyping | Tailwind CSS | Fast development, no naming |
| Component library | CSS Modules or Styled Components | Encapsulated, reusable styles |
| Dynamic theming | CSS-in-JS (Styled Components) | Theme props, JavaScript power |
| Large team, design system | Tailwind or CSS Modules | Consistency, scalability |
| Marketing site | Plain CSS or Tailwind | Performance, SEO |
| Complex web app | CSS Modules + Tailwind combo | Structure + flexibility |
Mixing Approaches
You can combine multiple styling methods in one project:
// Global styles with plain CSS
import './index.css';
// Component styles with CSS Modules
import styles from './Button.module.css';
// Dynamic styles with inline
const dynamicStyle = {
width: `${percentage}%`
};
// Utility classes with Tailwind
function Component() {
return (
<div className="flex gap-4"> {/* Tailwind */}
<button
className={styles.button} {/* CSS Module */}
style={dynamicStyle} {/* Inline */}
>
Click me
</button>
</div>
);
}
Performance Considerations
- Best Performance: Plain CSS, CSS Modules (compiled at build time)
- Good Performance: Tailwind (purges unused styles)
- Runtime Overhead: CSS-in-JS (generates styles in browser)
- Small Impact: Inline styles (for dynamic values only)
Team Considerations
- Junior developers: Plain CSS or Tailwind (familiar concepts)
- Mixed CSS/JS skills: CSS Modules (separation of concerns)
- JavaScript-focused team: CSS-in-JS (everything in JS)
- Large teams: Design system + Tailwind (consistency)
β Our Recommendation for Beginners
Start with CSS Modules - they offer the best balance of:
- Familiar CSS syntax
- Automatic scoping
- No new dependencies
- Great performance
- Easy to learn
Once comfortable, explore Tailwind for speed or styled-components for advanced needs.
π Summary
π Congratulations!
You've mastered styling in React! Here's what you now know:
- Styling approaches - Plain CSS, CSS Modules, inline, CSS-in-JS, Tailwind
- CSS Modules - Scoped styles with .module.css files
- Inline styles - JavaScript objects for dynamic styling
- Conditional styling - Applying styles based on props/state
- CSS-in-JS - styled-components and theme providers
- Tailwind CSS - Utility-first rapid development
- Responsive design - Media queries and mobile-first approach
- Choosing approaches - When to use each method
π― Key Takeaways
- No single "best" approach - choose based on needs
- CSS Modules prevent naming conflicts automatically
- Inline styles perfect for dynamic values
- CSS-in-JS enables powerful theming
- Tailwind accelerates development
- You can mix multiple approaches
- Mobile-first is the modern standard
- Performance matters - choose wisely
Quick Reference
| Method | Syntax | Best For |
|---|---|---|
| Plain CSS | className="btn" |
Global styles, simple projects |
| CSS Modules | className={styles.btn} |
Component-scoped styles |
| Inline | style={{'{color: "red"}'}} |
Dynamic values |
| Styled Components | const Btn = styled.button`` |
Theming, CSS-in-JS |
| Tailwind | className="px-4 py-2" |
Rapid development |
π Additional Resources
- React Docs: Styling React Components
- CSS Modules Documentation
- styled-components Official Site
- Tailwind CSS Documentation
- Responsive Web Design Basics
π What's Next?
In the next lesson, we'll explore Events in React. You'll learn:
- Handling user interactions (clicks, inputs, forms)
- Event handling best practices
- Synthetic events in React
- Event delegation and bubbling
- Form handling and validation
- Keyboard and accessibility events
Your components look great - now let's make them interactive! π―
π Styling Maestro!
You now have a complete toolkit for styling React applications! From traditional CSS to modern utility-first frameworks, you can choose the right tool for any project. Your components will look professional and polished!
Time to make them interactive! π