Progressive Web Apps (PWA)

Responsive Design for PWAs

16 min Lesson 13 of 30

Introduction to Responsive PWA Design

Progressive Web Apps must provide an excellent experience on all devices. Responsive design ensures your PWA looks and works great on phones, tablets, desktops, and everything in between.

Mobile-First Design Philosophy

Mobile-first design starts with designing for the smallest screen and progressively enhancing for larger screens:

Why Mobile-First?
  • Forces focus on essential content and features
  • Better performance on mobile devices
  • Easier to scale up than scale down
  • Most users access PWAs on mobile devices first
/* Mobile-first CSS approach */ /* Base styles for mobile (no media query needed) */ .container { width: 100%; padding: 1rem; } .grid { display: flex; flex-direction: column; gap: 1rem; } /* Tablet styles (768px and up) */ @media (min-width: 768px) { .container { max-width: 720px; margin: 0 auto; } .grid { flex-direction: row; flex-wrap: wrap; } .grid-item { flex: 0 0 calc(50% - 0.5rem); } } /* Desktop styles (1024px and up) */ @media (min-width: 1024px) { .container { max-width: 960px; } .grid-item { flex: 0 0 calc(33.333% - 0.667rem); } } /* Large desktop styles (1280px and up) */ @media (min-width: 1280px) { .container { max-width: 1200px; } }

The Viewport Meta Tag

The viewport meta tag is crucial for responsive design. It controls how your PWA is displayed on mobile devices:

<!-- Recommended viewport settings for PWAs --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes">

Let's break down each property:

  • width=device-width: Sets viewport width to the device's width
  • initial-scale=1: Sets initial zoom level to 100%
  • maximum-scale=5: Allows users to zoom up to 500% (accessibility)
  • user-scalable=yes: Allows pinch-to-zoom (accessibility)
Warning: Never set user-scalable=no or maximum-scale=1 as this prevents users with visual impairments from zooming. This is an accessibility violation.

Responsive Images

Images should adapt to different screen sizes and resolutions:

1. Using srcset for Different Resolutions

<!-- Serve different image sizes based on screen density --> <img src="image-400.jpg" srcset="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w" sizes="(max-width: 600px) 100vw, (max-width: 900px) 50vw, 33vw" alt="Responsive image" loading="lazy">

2. Using picture Element for Art Direction

<!-- Different images for different screen sizes --> <picture> <source media="(max-width: 600px)" srcset="hero-mobile.jpg"> <source media="(max-width: 1200px)" srcset="hero-tablet.jpg"> <img src="hero-desktop.jpg" alt="Hero image" loading="lazy"> </picture>

3. CSS Background Images

/* Responsive background images */ .hero { background-image: url('hero-mobile.jpg'); background-size: cover; background-position: center; } @media (min-width: 768px) { .hero { background-image: url('hero-tablet.jpg'); } } @media (min-width: 1200px) { .hero { background-image: url('hero-desktop.jpg'); } } /* For high-DPI screens */ @media (min-width: 1200px) and (-webkit-min-device-pixel-ratio: 2), (min-width: 1200px) and (min-resolution: 192dpi) { .hero { background-image: url('hero-desktop@2x.jpg'); } }

Responsive Typography

Text should be readable on all screen sizes without zooming:

/* Base font size (mobile) */ html { font-size: 16px; /* Never go below 16px for body text */ } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; line-height: 1.6; } /* Fluid typography using clamp() */ h1 { font-size: clamp(2rem, 5vw, 3.5rem); line-height: 1.2; } h2 { font-size: clamp(1.5rem, 4vw, 2.5rem); line-height: 1.3; } p { font-size: clamp(1rem, 2vw, 1.125rem); max-width: 70ch; /* Optimal reading width */ } /* Responsive spacing */ .section { padding: clamp(2rem, 5vw, 4rem) 1rem; }

Flexible Layouts with CSS Grid and Flexbox

Responsive Grid Layout

/* Auto-responsive grid */ .grid-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; padding: 1rem; } /* Responsive grid with named areas */ .app-layout { display: grid; grid-template-areas: "header" "nav" "main" "sidebar" "footer"; gap: 1rem; } @media (min-width: 768px) { .app-layout { grid-template-areas: "header header" "nav nav" "main sidebar" "footer footer"; grid-template-columns: 2fr 1fr; } } @media (min-width: 1024px) { .app-layout { grid-template-areas: "header header header" "nav main sidebar" "footer footer footer"; grid-template-columns: 200px 2fr 1fr; } } .header { grid-area: header; } .nav { grid-area: nav; } .main { grid-area: main; } .sidebar { grid-area: sidebar; } .footer { grid-area: footer; }

Responsive Flexbox Layout

/* Flexible card layout */ .card-container { display: flex; flex-wrap: wrap; gap: 1rem; padding: 1rem; } .card { flex: 1 1 100%; /* Full width on mobile */ min-width: 0; /* Allow flex items to shrink below content size */ } @media (min-width: 600px) { .card { flex: 1 1 calc(50% - 0.5rem); /* Two columns on tablet */ } } @media (min-width: 900px) { .card { flex: 1 1 calc(33.333% - 0.667rem); /* Three columns on desktop */ } }

Touch Targets and Mobile Interactions

Touch targets should be large enough for comfortable tapping:

/* Minimum touch target size: 44x44px (iOS) or 48x48px (Android) */ .button, .link, .touch-target { min-width: 48px; min-height: 48px; display: inline-flex; align-items: center; justify-content: center; padding: 0.75rem 1.5rem; } /* Adequate spacing between touch targets */ .nav-list { display: flex; gap: 0.5rem; /* At least 8px between targets */ } /* Remove hover effects on touch devices */ @media (hover: none) { .button:hover { /* Remove or modify hover styles */ background-color: inherit; } } /* Enable hover effects only on devices that support it */ @media (hover: hover) { .button:hover { background-color: #007bff; transform: translateY(-2px); } } /* Touch-specific interactions */ .swipeable { touch-action: pan-y; /* Allow vertical scrolling only */ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ }

PWA-Specific Responsive Patterns

1. Bottom Navigation (Mobile Pattern)

/* Mobile bottom navigation */ .bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; display: flex; justify-content: space-around; background: white; border-top: 1px solid #ddd; padding: 0.5rem; z-index: 1000; } .bottom-nav-item { display: flex; flex-direction: column; align-items: center; min-width: 48px; padding: 0.5rem; text-decoration: none; color: #666; } /* Switch to sidebar on larger screens */ @media (min-width: 768px) { .bottom-nav { position: static; flex-direction: column; width: 200px; border-top: none; border-right: 1px solid #ddd; } }

2. Responsive Header with Hamburger Menu

<!-- HTML structure --> <header class="app-header"> <div class="header-container"> <button class="menu-toggle" aria-label="Toggle menu"> <span class="hamburger"></span> </button> <h1 class="logo">My PWA</h1> <nav class="main-nav"> <ul class="nav-list"> <li><a href="#home">Home</a></li> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> </ul> </nav> </div> </header>
/* Mobile menu (hidden by default) */ .menu-toggle { display: block; background: none; border: none; min-width: 44px; min-height: 44px; } .main-nav { position: fixed; top: 60px; left: -100%; width: 80%; max-width: 300px; height: calc(100vh - 60px); background: white; transition: left 0.3s ease; box-shadow: 2px 0 10px rgba(0,0,0,0.1); } .main-nav.active { left: 0; } .nav-list { list-style: none; padding: 1rem; } .nav-list a { display: block; padding: 1rem; min-height: 48px; } /* Desktop menu (always visible) */ @media (min-width: 768px) { .menu-toggle { display: none; } .main-nav { position: static; width: auto; height: auto; background: transparent; box-shadow: none; } .nav-list { display: flex; gap: 1rem; } }

3. Responsive Cards with CSS Grid

.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 1.5rem; padding: 1rem; } .card { display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .card-image { width: 100%; aspect-ratio: 16/9; object-fit: cover; } .card-content { padding: 1rem; flex: 1; }

Safe Area Insets (Notched Devices)

Handle notches and safe areas on modern mobile devices:

/* Add safe area insets to viewport meta */ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
/* Use CSS environment variables for safe areas */ .app-header { position: fixed; top: 0; left: 0; right: 0; padding: 1rem; padding-top: max(1rem, env(safe-area-inset-top)); padding-left: max(1rem, env(safe-area-inset-left)); padding-right: max(1rem, env(safe-area-inset-right)); } .app-footer { padding-bottom: max(1rem, env(safe-area-inset-bottom)); } /* Full bleed sections */ .full-bleed { width: 100vw; margin-left: calc(-1 * env(safe-area-inset-left)); margin-right: calc(-1 * env(safe-area-inset-right)); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
Exercise:
  1. Create a responsive PWA layout with:
    • Mobile-first CSS starting at 320px
    • Proper viewport meta tag
    • Responsive navigation (hamburger on mobile, horizontal on desktop)
    • Responsive grid of cards (1 column mobile, 2 tablet, 3 desktop)
    • Touch-friendly buttons (min 48x48px)
  2. Implement responsive images using srcset and picture elements
  3. Add fluid typography using clamp()
  4. Test on multiple devices and screen sizes
  5. Ensure all interactive elements meet touch target size requirements
Tip: Use browser DevTools device emulation to test your responsive design. Chrome DevTools has a responsive mode that lets you test various device sizes and even throttle network and CPU to simulate real mobile conditions.