π¨ 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 a traditional website β where the markup lives in HTML and the styles live in a separate stylesheet across the project β a React component can keep its markup, its logic, and its styles together. This "component-based styling" is a genuine shift in how we organize UI: styles travel with the component that owns them, so a button and its look move, copy, and delete as a single self-contained unit.
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!
π 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;
}
Composing Class Names with a Helper
Once your class logic grows past a simple template literal, reach for a tiny helper. In 2026 the go-to is clsx β it's roughly 200 bytes, ships its own TypeScript types (no separate @types package to install), and has the same API as the older classnames. It joins string arguments and includes object keys whose value is truthy:
# clsx bundles its own types β nothing extra to install
npm install clsx
import clsx from 'clsx';
import './Button.css';
function Button({
text,
variant = 'primary',
size = 'medium',
disabled = false,
loading = false,
onClick
}: ButtonProps) {
const buttonClasses = clsx('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 whose class names are scoped locally by default. At build time your bundler (Vite, Next.js, Parcel β all support them out of the box) rewrites each class name to a unique string, so two files can both declare .title without ever colliding.
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 clsx
Combine CSS Modules with the clsx helper β you pass the scoped styles.* values instead of raw strings:
import clsx from 'clsx';
import styles from './Button.module.css';
function Button({
variant = 'primary',
size = 'medium',
disabled = false
}: ButtonProps) {
const buttonClasses = clsx(
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
Out of the box, TypeScript types a .module.css import loosely as { readonly [key: string]: string } β so styles.anything compiles even if that class doesn't exist. To get real autocomplete and catch typos, add the editor plugin:
# Install the TypeScript language-service plugin
npm install --save-dev typescript-plugin-css-modules
// tsconfig.json
{
"compilerOptions": {
"plugins": [{ "name": "typescript-plugin-css-modules" }]
}
}
π‘ 2026 note
The plugin powers editor autocomplete but doesn't fail a tsc build. If you want the compiler itself to reject unknown class names, generate real .d.ts files for each module with a tool such as typed-css-modules, or lean on a typed styling layer like Vanilla Extract or Panda CSS where the class names are TypeScript values to begin with.
With the plugin installed, your editor knows exactly which classes exist:
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
π‘ Type your style objects with React.CSSProperties
The style prop is typed as React.CSSProperties β an interface with every valid camelCase CSS property. Annotate your style objects with it and TypeScript checks each property and gives you autocomplete for the values:
const boxStyle: React.CSSProperties = {
backgroundColor: '#667eea',
padding: 16, // β
numbers are OK β React adds 'px' for lengths
borderRadius: '8px',
// 'background-color': 'red' // β hyphenated key is not assignable
// display: 'flx', // β not a valid value for 'display'
};
Numbers are allowed for length-style properties (React appends px), but a handful of properties are unitless β lineHeight, zIndex, flexGrow, opacity β so lineHeight: 1.6 stays 1.6, not 1.6px.
Check Yourself: Is It a Valid Style Object?
Click each candidate below to see whether it would satisfy React.CSSProperties β and if not, why the compiler rejects it.
const style: React.CSSProperties = { β¦ }
π Click an example above to see what TypeScript thinks.
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 them outside the component so they aren't recreated on every render, and type them so TypeScript validates every property:
// Typed with React.CSSProperties β no `as const` gymnastics needed.
// Without the annotation, 'column' would widen to `string` and error.
const styles: Record<string, React.CSSProperties> = {
container: {
display: 'flex',
flexDirection: 'column', // β
checked against the allowed union
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: clsx helper (recommended)
const classNameBest = clsx('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>
);
}
Live Playground: A Box Driven by Inline Styles
Dynamic styling shines when the numbers come from real input. Move the sliders below β each one feeds a value straight into a React.CSSProperties object applied to the preview box, exactly the way useState values would in a real component:
const style: React.CSSProperties = {
width: 100,
height: 100,
borderRadius: 8,
backgroundColor: '#667eea',
transform: 'rotate(0deg)',
};
Notice there's no CSS file here at all β the object is the styling, computed live from JavaScript. That's the sweet spot for inline styles: values that genuinely change at runtime, like this box, a progress bar's width, or a user-picked accent color.
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. For years the most popular was styled-components, and it's still a great way to learn the pattern β so we'll use it as our example below.
β οΈ A 2026 reality check on runtime CSS-in-JS
The libraries in this section β styled-components and Emotion β are runtime CSS-in-JS: they generate and inject styles in the browser as your components render. Two things have cooled the industry on that approach:
- React Server Components. Runtime CSS-in-JS needs the browser and React Context, so it forces components to be Client Components. That fights the default in modern frameworks (Next.js App Router), where you want to render on the server.
- styled-components is now in maintenance mode (announced by its maintainers in 2025) β it still works, but it isn't getting new features.
The momentum has shifted to zero-runtime options that produce plain CSS at build time: CSS Modules, Tailwind, Vanilla Extract, Linaria, Panda CSS, and StyleX. Learn the concept here; for a brand-new app in 2026, reach for a zero-runtime tool first.
Introduction to styled-components
styled-components lets you create components with built-in styles:
# styled-components v6 bundles its own types β no @types package needed
npm install 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 (v4)
Tailwind CSS v4 (released 2025) is a big simplification: there's no tailwind.config.js to generate and no PostCSS/autoprefixer setup. For a Vite project you add the official plugin and a single CSS import:
# Tailwind v4 with the Vite plugin
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react(), tailwindcss()],
});
/* index.css β one line replaces the old three @tailwind directives */
@import "tailwindcss";
π‘ v3 vs v4
If you land in an older codebase you'll see the v3 setup instead: a tailwind.config.js with a content array, npx tailwindcss init -p, and the three @tailwind base; @tailwind components; @tailwind utilities; directives. In v4 you configure your design tokens right in CSS with a @theme { β¦ } block instead of a JS config file.
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 change the simulated viewport width. Watch the grid reflow the way a mobile-first CSS layout would β one column on a phone, more columns as space allows:
Viewport: 800px β Tablet Β· 2 columns
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 |
A Quick Decision Flow
When you're not sure, this flowchart gets you to a sensible default in three questions:
runtime (props/state)?"} B -->|"Yes, lots of it"| D["Inline style prop
(typed React.CSSProperties)"] B -->|"Mostly static"| C{"Want zero-config
utility classes?"} C -->|"Yes"| E["Tailwind CSS"] C -->|"No, prefer real CSS"| F["CSS Modules"] D --> G["Ship it"] E --> G F --> G style D fill:#FF9800,stroke:#333,stroke-width:2px,color:#fff style E fill:#06B6D4,stroke:#333,stroke-width:2px,color:#fff style F fill:#2196F3,stroke:#333,stroke-width:2px,color:#fff style G fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff
Notice runtime CSS-in-JS isn't the default recommendation anymore β reach for it only when you specifically need JavaScript-driven theming and you're not in a React Server Components app.
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
- Type your inline styles with
React.CSSProperties - Runtime CSS-in-JS is fading β prefer zero-runtime tools
- 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! π