JavaScript and React course Β· Module 14: Advanced Styling

Responsive Design Patterns in React - Adapting to Every Space

4 min read
In this lesson7

Pilot, your spaceship must work just as well on a small handheld terminal screen as it does on the large monitor of a command center. In the React world, responsive design means creating components that elegantly adapt to any screen size -- from mobile devices to ultra-wide monitors.

useMediaQuery Hook -- responsiveness in JavaScript

Sometimes you need to change not just styles but the entire component structure depending on screen size. That's when a custom useMediaQuery hook comes in handy:

1function useMediaQuery(query) {
2  const [matches, setMatches] = useState(
3    () => window.matchMedia(query).matches
4  );
5
6  useEffect(() => {
7    const mediaQuery = window.matchMedia(query);
8    const handler = (e) => setMatches(e.matches);
9
10    mediaQuery.addEventListener('change', handler);
11    return () => mediaQuery.removeEventListener('change', handler);
12  }, [query]);
13
14  return matches;
15}

Usage in a component:

1function MissionDashboard() {
2  const isMobile = useMediaQuery('(max-width: 767px)');
3  const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1023px)');
4
5  if (isMobile) return <MobileDashboard />;
6  if (isTablet) return <TabletDashboard />;
7  return <DesktopDashboard />;
8}

The hook listens for window size changes and returns true/false -- React automatically re-renders the component when the value changes.

Mobile-first CSS in React

The mobile-first approach means: we start with styles for the smallest screens and then expand for larger ones. Like designing a cockpit from the simplest terminal upward:

1/* Base styles -- mobile */
2.grid {
3  display: grid;
4  grid-template-columns: 1fr;
5  gap: 16px;
6}
7
8/* Tablet -- expanding */
9@media (min-width: 768px) {
10  .grid {
11    grid-template-columns: repeat(2, 1fr);
12  }
13}
14
15/* Desktop -- even wider */
16@media (min-width: 1024px) {
17  .grid {
18    grid-template-columns: repeat(3, 1fr);
19  }
20}

Responsive component composition

Instead of one huge component with many conditions, it's better to split the UI into smaller parts that can be composed differently:

1function Sidebar({ items }) {
2  return (
3    <nav className="sidebar">
4      {items.map(item => (
5        <a key={item.id} href={item.href}>{item.label}</a>
6      ))}
7    </nav>
8  );
9}
10
11function BottomNav({ items }) {
12  return (
13    <nav className="bottom-nav">
14      {items.map(item => (
15        <a key={item.id} href={item.href}>{item.icon}</a>
16      ))}
17    </nav>
18  );
19}
20
21function Navigation({ items }) {
22  const isMobile = useMediaQuery('(max-width: 767px)');
23  return isMobile ? <BottomNav items={items} /> : <Sidebar items={items} />;
24}

On mobile, the navigation is at the bottom of the screen (like in an app), and on desktop -- on the side as a sidebar. Same data set, different presentation.

Container Queries in React

Container queries are a powerful new CSS feature. They allow you to style an element based on the size of its parent, not the entire browser window:

1.card-container {
2  container-type: inline-size;
3}
4
5@container (min-width: 400px) {
6  .card {
7    display: flex;
8    gap: 16px;
9  }
10}
11
12@container (min-width: 600px) {
13  .card {
14    grid-template-columns: 1fr 2fr;
15  }
16}

In React, we set container-type on the parent:

1function CardGrid({ children }) {
2  return (
3    <div style={{ containerType: 'inline-size' }}>
4      {children}
5    </div>
6  );
7}

Container queries are great for reusable components -- a mission card can look different in a narrow sidebar and a wide main content area, without knowing the window size.

Fluid Typography

Instead of changing font size in steps between breakpoints, you can use clamp() for smooth scaling:

1.heading {
2  /* Minimum 18px, optimal 4vw, maximum 36px */
3  font-size: clamp(18px, 4vw, 36px);
4}
5
6.body-text {
7  font-size: clamp(14px, 2vw, 18px);
8  line-height: clamp(20px, 3vw, 28px);
9}

In React, you can implement this as a system:

1const fluidType = {
2  heading: 'clamp(18px, 4vw, 36px)',
3  subheading: 'clamp(16px, 3vw, 24px)',
4  body: 'clamp(14px, 2vw, 18px)',
5  caption: 'clamp(11px, 1.5vw, 14px)',
6};
7
8function Heading({ children }) {
9  return <h1 style={{ fontSize: fluidType.heading }}>{children}</h1>;
10}

Responsive Images in React

Images are often the heaviest assets on a page. React allows you to implement responsive images:

1function ResponsiveImage({ src, alt, sizes }) {
2  return (
3    <img
4      src={src}
5      alt={alt}
6      srcSet={`${src}?w=400 400w, ${src}?w=800 800w, ${src}?w=1200 1200w`}
7      sizes={sizes || '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'}
8      loading="lazy"
9      style={{ width: '100%', height: 'auto' }}
10    />
11  );
12}

The srcSet attribute tells the browser about available sizes, and sizes -- which size to use at a given window size.

When CSS, when JavaScript?

  • Media queries in CSS -- when you're only changing styles (colors, sizes, layout)
  • useMediaQuery in JS -- when you're changing the component structure (different components, different logic)
  • Container queries -- when a component needs to react to the size of its container
  • clamp() -- for smooth scaling of values between a minimum and maximum
Code for this lesson: App.jsx
1import React, { useState, useEffect } from 'react';
2import './styles.css';
3
4// useMediaQuery hook -- listens for window size changes
5function useMediaQuery(query) {
6  const [matches, setMatches] = useState(
7    () => window.matchMedia(query).matches
8  );
9
10  useEffect(() => {
11    const mq = window.matchMedia(query);
12    const handler = (e) => setMatches(e.matches);
13    mq.addEventListener('change', handler);
14    return () => mq.removeEventListener('change', handler);
15  }, [query]);
16
17  return matches;
18}
19
20// Mission card -- different content on mobile and desktop
21function MissionCard({ name, status, crew, description, stats }) {
22  const isMobile = useMediaQuery('(max-width: 500px)');
23
24  return (
25    <div className="mission-card">
26      <div className="card-header">
27        <h3 className="card-name">{name}</h3>
28        <span className={`badge badge-${status}`}>{status}</span>
29      </div>
30      <p className="card-desc">{description}</p>
31      {!isMobile && (
32        <div className="stats-row">
33          {stats.map(s => (
34            <div key={s.label} className="stat">
35              <span className="stat-value">{s.value}</span>
36              <span className="stat-label">{s.label}</span>
37            </div>
38          ))}
39        </div>
40      )}
41      <div className="card-footer">
42        <span className="crew">Crew: {crew}</span>
43        {!isMobile && <button className="btn">Details</button>}
44      </div>
45    </div>
46  );
47}
48
49// Navigation -- sidebar on desktop, bottom nav on mobile
50function Navigation({ items, active, onSelect }) {
51  const isMobile = useMediaQuery('(max-width: 500px)');
52
53  if (isMobile) {
54    return (
55      <nav className="bottom-nav">
56        {items.map(item => (
57          <button
58            key={item.id}
59            className={`nav-item ${active === item.id ? 'nav-active' : ''}`}
60            onClick={() => onSelect(item.id)}
61          >
62            {item.icon} {item.label}
63          </button>
64        ))}
65      </nav>
66    );
67  }
68
69  return (
70    <nav className="sidebar">
71      <h2 className="sidebar-title">Navigation</h2>
72      {items.map(item => (
73        <button
74          key={item.id}
75          className={`sidebar-item ${active === item.id ? 'sidebar-active' : ''}`}
76          onClick={() => onSelect(item.id)}
77        >
78          {item.icon} {item.label}
79        </button>
80      ))}
81    </nav>
82  );
83}
84
85const navItems = [
86  { id: 'missions', label: 'Missions', icon: '' },
87  { id: 'crew', label: 'Crew', icon: '' },
88  { id: 'systems', label: 'Systems', icon: '' },
89];
90
91const missions = [
92  { name: 'Apollo X', status: 'active', crew: 4, description: 'Mars Exploration',
93    stats: [{ label: 'Distance', value: '225M km' }, { label: 'Time', value: '7 mo.' }, { label: 'Progress', value: '67%' }] },
94  { name: 'Nebula Run', status: 'planning', crew: 6, description: 'Mission to the Orion Nebula',
95    stats: [{ label: 'Distance', value: '1344 ly' }, { label: 'Time', value: '2 years' }, { label: 'Progress', value: '12%' }] },
96];
97
98export default function App() {
99  const [activeNav, setActiveNav] = useState('missions');
100  const isMobile = useMediaQuery('(max-width: 500px)');
101
102  return (
103    <div className="app">
104      <div className="layout">
105        {!isMobile && (
106          <Navigation items={navItems} active={activeNav} onSelect={setActiveNav} />
107        )}
108        <main className="main">
109          <h1 className="heading">Responsive Design w React</h1>
110          <p className="subtitle">
111            {isMobile ? 'Mobile view' : 'Desktop view'} --
112            resize the preview window!
113          </p>
114          <div className="missions-grid">
115            {missions.map(m => <MissionCard key={m.name} {...m} />)}
116          </div>
117        </main>
118      </div>
119      {isMobile && (
120        <Navigation items={navItems} active={activeNav} onSelect={setActiveNav} />
121      )}
122    </div>
123  );
124}

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the purpose of the custom useMediaQuery hook in React?

  2. 2. What does the mobile-first approach mean in responsive design?

These are 2 of 4 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Horizontal ordering

    Arrange the media query syntax for screens from 768px:

  • Code editor

    Implement responsive navigation with useMediaQuery

  • Click in order

    Arrange the fluid typography syntax using clamp() in CSS:

  • Vertical ordering

    Arrange the typical mobile-first breakpoints from smallest to largest:

  • Code editor

    Implement a mission card with different content on mobile and desktop

Useful articles