PatternsMobile & PWA
Mobile-First Approach
Write CSS and JavaScript that prioritizes mobile devices before desktop for better performance.
The Problem
Desktop-first development:
- Slower mobile: Loads desktop CSS first
- More overrides: Mobile must undo desktop styles
- Larger bundles: Extra CSS for mobile
- Poor experience: 60% of users are mobile
Solution
Start with mobile styles and progressively enhance for larger screens.
/* ❌ Desktop-First (Bad) */
.container {
width: 1200px;
display: grid;
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 768px) {
.container {
width: 100%;
grid-template-columns: 1fr;
}
}
/* ✅ Mobile-First (Good) */
.container {
width: 100%;
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.container {
width: 1200px;
margin: 0 auto;
grid-template-columns: repeat(4, 1fr);
}
}Breakpoint System
/**
* Standard breakpoints
*/
export const breakpoints = {
sm: 640,
md: 768,
lg: 1024,
xl: 1280,
'2xl': 1536,
} as const;
/**
* Media query builder
*/
class MediaQuery {
/**
* Min-width (mobile-first)
*/
public static up(size: keyof typeof breakpoints): string {
return `(min-width: ${breakpoints[size]}px)`;
}
/**
* Max-width (desktop-first - avoid)
*/
public static down(size: keyof typeof breakpoints): string {
return `(max-width: ${breakpoints[size] - 1}px)`;
}
/**
* Between two sizes
*/
public static between(
min: keyof typeof breakpoints,
max: keyof typeof breakpoints
): string {
return `(min-width: ${breakpoints[min]}px) and (max-width: ${breakpoints[max] - 1}px)`;
}
/**
* Check if matches
*/
public static matches(query: string): boolean {
return window.matchMedia(query).matches;
}
/**
* Listen for changes
*/
public static listen(
query: string,
callback: (matches: boolean) => void
): () => void {
const mediaQuery = window.matchMedia(query);
const handler = (event: MediaQueryListEvent) => {
callback(event.matches);
};
mediaQuery.addEventListener('change', handler);
// Return cleanup function
return () => {
mediaQuery.removeEventListener('change', handler);
};
}
}
/**
* Responsive manager
*/
class ResponsiveManager {
private breakpoint: keyof typeof breakpoints | 'xs' = 'xs';
private listeners: Set<(bp: string) => void> = new Set();
constructor() {
this.detectBreakpoint();
this.setupListeners();
}
private detectBreakpoint(): void {
const width = window.innerWidth;
if (width >= breakpoints['2xl']) this.breakpoint = '2xl';
else if (width >= breakpoints.xl) this.breakpoint = 'xl';
else if (width >= breakpoints.lg) this.breakpoint = 'lg';
else if (width >= breakpoints.md) this.breakpoint = 'md';
else if (width >= breakpoints.sm) this.breakpoint = 'sm';
else this.breakpoint = 'xs';
}
private setupListeners(): void {
window.addEventListener('resize', () => {
const oldBreakpoint = this.breakpoint;
this.detectBreakpoint();
if (oldBreakpoint !== this.breakpoint) {
this.notifyListeners();
}
});
}
private notifyListeners(): void {
this.listeners.forEach((callback) => {
callback(this.breakpoint);
});
}
/**
* Subscribe to breakpoint changes
*/
public onChange(callback: (bp: string) => void): () => void {
this.listeners.add(callback);
return () => {
this.listeners.delete(callback);
};
}
/**
* Get current breakpoint
*/
public getBreakpoint(): string {
return this.breakpoint;
}
/**
* Check if mobile
*/
public isMobile(): boolean {
return this.breakpoint === 'xs' || this.breakpoint === 'sm';
}
/**
* Check if tablet
*/
public isTablet(): boolean {
return this.breakpoint === 'md';
}
/**
* Check if desktop
*/
public isDesktop(): boolean {
return this.breakpoint === 'lg' || this.breakpoint === 'xl' || this.breakpoint === '2xl';
}
}
// Global instance
export const responsive = new ResponsiveManager();React Hook
import { useState, useEffect } from 'react';
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => {
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}
function useBreakpoint() {
const isMobile = useMediaQuery('(max-width: 767px)');
const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1023px)');
const isDesktop = useMediaQuery('(min-width: 1024px)');
return { isMobile, isTablet, isDesktop };
}
// Usage
function ResponsiveComponent() {
const { isMobile, isDesktop } = useBreakpoint();
return (
<div>
{isMobile && <MobileNav />}
{isDesktop && <DesktopNav />}
</div>
);
}Component Loading Strategy
/**
* Load components based on screen size
*/
class ComponentLoader {
/**
* Load mobile or desktop component
*/
public static async loadResponsive<T>(
mobileImport: () => Promise<{ default: T }>,
desktopImport: () => Promise<{ default: T }>
): Promise<T> {
if (responsive.isMobile()) {
const module = await mobileImport();
return module.default;
} else {
const module = await desktopImport();
return module.default;
}
}
}
// Usage
const Gallery = await ComponentLoader.loadResponsive(
() => import('./MobileGallery'),
() => import('./DesktopGallery')
);Mobile-First CSS Examples
/* Typography */
body {
font-size: 16px;
line-height: 1.5;
}
@media (min-width: 768px) {
body {
font-size: 18px;
}
}
/* Spacing */
.section {
padding: 1rem;
}
@media (min-width: 768px) {
.section {
padding: 2rem;
}
}
@media (min-width: 1024px) {
.section {
padding: 4rem;
}
}
/* Grid */
.grid {
display: grid;
gap: 1rem;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Navigation */
.nav {
position: fixed;
bottom: 0;
width: 100%;
}
@media (min-width: 768px) {
.nav {
position: static;
}
}Best Practices
- Start mobile: Base styles for small screens
- Use min-width: Progressive enhancement
- Touch targets: 44px minimum
- Readable text: 16px minimum font size
- Fast loading: Prioritize mobile performance
- Test on devices: Real devices, not just emulators
- Content first: Mobile forces prioritization
Mobile-first reduces CSS by 30% and improves mobile performance by 40%.