Front-end Engineering Lab
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

  1. Start mobile: Base styles for small screens
  2. Use min-width: Progressive enhancement
  3. Touch targets: 44px minimum
  4. Readable text: 16px minimum font size
  5. Fast loading: Prioritize mobile performance
  6. Test on devices: Real devices, not just emulators
  7. Content first: Mobile forces prioritization

Mobile-first reduces CSS by 30% and improves mobile performance by 40%.

On this page