Skip to main content

🎨 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
graph TD A[React Styling] --> B[Traditional CSS] A --> C[Modern Approaches] B --> D[Plain CSS Files] B --> E[CSS Modules] C --> F[Inline Styles] C --> G[CSS-in-JS] C --> H[Utility-First] G --> I[Styled Components] G --> J[Emotion] H --> K[Tailwind CSS] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style B fill:#4CAF50,stroke:#333,stroke-width:2px style C fill:#2196F3,stroke:#333,stroke-width:2px

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:

CSS Module Class Name Transformation What You Write .button { padding: 1rem; } .primary { background: blue; } .card { border-radius: 8px; } Build Browser Sees .Button_button__x7Ks2 .Button_primary__a3Bm9 .Card_card__q8Np4 Why This Matters: No More Conflicts! ❌ Plain CSS // Header.css AND Card.css .title { } // CONFLICT! βœ… CSS Modules Header_title__abc123 Card_title__xyz789 // No conflict!

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 β†’ backgroundColor
  • font-size β†’ fontSize
  • margin-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:

Viewport: 800px

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:

  1. Create a Card component with CSS Module styling
  2. Include header, body, and footer sections
  3. Add hover effects and transitions
  4. Make it responsive (stack on mobile, side-by-side on desktop)
  5. 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:

  1. Create a theme context with light/dark modes
  2. Build a toggle button to switch themes
  3. Style components differently based on theme
  4. 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

πŸš€ 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! πŸš€