Front-end Engineering Lab
PatternsDesign Systems

Responsive Design Tokens

Build responsive systems with design tokens and breakpoints

Responsive Design Tokens

Responsive design tokens adapt your UI to different screen sizes, creating consistent experiences across devices.

Breakpoint Tokens

// tokens/breakpoints.ts
export const breakpoints = {
  xs: '320px',   // Mobile portrait
  sm: '640px',   // Mobile landscape
  md: '768px',   // Tablet portrait
  lg: '1024px',  // Tablet landscape / Desktop
  xl: '1280px',  // Desktop
  '2xl': '1536px', // Large desktop
};

// Numeric values for JS
export const breakpointValues = {
  xs: 320,
  sm: 640,
  md: 768,
  lg: 1024,
  xl: 1280,
  '2xl': 1536,
};

Responsive Spacing

// tokens/spacing.ts
export const spacing = {
  // Base scale
  0: '0',
  1: '0.25rem',  // 4px
  2: '0.5rem',   // 8px
  3: '0.75rem',  // 12px
  4: '1rem',     // 16px
  6: '1.5rem',   // 24px
  8: '2rem',     // 32px
  12: '3rem',    // 48px
  16: '4rem',    // 64px
  
  // Responsive spacing
  responsive: {
    xs: {
      gutter: '1rem',    // 16px
      section: '2rem',   // 32px
    },
    sm: {
      gutter: '1.5rem',  // 24px
      section: '3rem',   // 48px
    },
    md: {
      gutter: '2rem',    // 32px
      section: '4rem',   // 64px
    },
    lg: {
      gutter: '2.5rem',  // 40px
      section: '5rem',   // 80px
    },
    xl: {
      gutter: '3rem',    // 48px
      section: '6rem',   // 96px
    },
  },
};

Responsive Typography

// tokens/typography.ts
export const typography = {
  fontSize: {
    // Mobile-first base sizes
    xs: '0.75rem',    // 12px
    sm: '0.875rem',   // 14px
    base: '1rem',     // 16px
    lg: '1.125rem',   // 18px
    xl: '1.25rem',    // 20px
    '2xl': '1.5rem',  // 24px
    '3xl': '1.875rem', // 30px
    '4xl': '2.25rem', // 36px
    '5xl': '3rem',    // 48px
  },
  
  // Responsive heading sizes
  headings: {
    h1: {
      mobile: '1.875rem',   // 30px
      tablet: '2.25rem',    // 36px
      desktop: '3rem',      // 48px
    },
    h2: {
      mobile: '1.5rem',     // 24px
      tablet: '1.875rem',   // 30px
      desktop: '2.25rem',   // 36px
    },
    h3: {
      mobile: '1.25rem',    // 20px
      tablet: '1.5rem',     // 24px
      desktop: '1.875rem',  // 30px
    },
  },
};

Container Tokens

// tokens/containers.ts
export const containers = {
  sm: '640px',
  md: '768px',
  lg: '1024px',
  xl: '1280px',
  '2xl': '1536px',
  
  // Content widths
  prose: '65ch',     // Optimal reading width
  narrow: '768px',
  wide: '1280px',
  full: '100%',
};

CSS Implementation

CSS Variables + Media Queries

/* tokens.css */
:root {
  /* Mobile-first (320px+) */
  --spacing-gutter: 1rem;
  --spacing-section: 2rem;
  --font-size-h1: 1.875rem;
}

/* Tablet (768px+) */
@media (min-width: 768px) {
  :root {
    --spacing-gutter: 2rem;
    --spacing-section: 4rem;
    --font-size-h1: 2.25rem;
  }
}

/* Desktop (1024px+) */
@media (min-width: 1024px) {
  :root {
    --spacing-gutter: 2.5rem;
    --spacing-section: 5rem;
    --font-size-h1: 3rem;
  }
}

/* Components use variables */
.section {
  padding: var(--spacing-section) var(--spacing-gutter);
}

.h1 {
  font-size: var(--font-size-h1);
}

Tailwind Configuration

// tailwind.config.js
import { breakpoints, spacing, typography } from './tokens';

export default {
  theme: {
    screens: breakpoints,
    
    extend: {
      spacing: spacing,
      
      fontSize: {
        ...typography.fontSize,
        // Responsive font sizes
        'h1-mobile': typography.headings.h1.mobile,
        'h1-desktop': typography.headings.h1.desktop,
      },
      
      maxWidth: {
        'prose': '65ch',
      },
    },
  },
};
// Usage with Tailwind responsive utilities
<h1 className="text-h1-mobile lg:text-h1-desktop">
  Responsive Heading
</h1>

<div className="px-4 md:px-8 lg:px-12">
  Responsive padding
</div>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  Responsive grid
</div>

React Hook for Breakpoints

// hooks/useBreakpoint.ts
import { useState, useEffect } from 'react';
import { breakpointValues } from '@/tokens/breakpoints';

type Breakpoint = keyof typeof breakpointValues;

export function useBreakpoint() {
  const [breakpoint, setBreakpoint] = useState<Breakpoint>('xs');

  useEffect(() => {
    const handleResize = () => {
      const width = window.innerWidth;
      
      if (width >= breakpointValues['2xl']) {
        setBreakpoint('2xl');
      } else if (width >= breakpointValues.xl) {
        setBreakpoint('xl');
      } else if (width >= breakpointValues.lg) {
        setBreakpoint('lg');
      } else if (width >= breakpointValues.md) {
        setBreakpoint('md');
      } else if (width >= breakpointValues.sm) {
        setBreakpoint('sm');
      } else {
        setBreakpoint('xs');
      }
    };

    handleResize(); // Initial
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return {
    breakpoint,
    isMobile: breakpoint === 'xs' || breakpoint === 'sm',
    isTablet: breakpoint === 'md',
    isDesktop: breakpoint === 'lg' || breakpoint === 'xl' || breakpoint === '2xl',
  };
}

// Usage
function Navigation() {
  const { isMobile } = useBreakpoint();

  return isMobile ? <MobileNav /> : <DesktopNav />;
}

Media Query Hook

// hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';

export function useMediaQuery(query: string): boolean {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
    setMatches(mediaQuery.matches);

    const handler = (event: MediaQueryListEvent) => {
      setMatches(event.matches);
    };

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  }, [query]);

  return matches;
}

// Usage
function Component() {
  const isLargeScreen = useMediaQuery('(min-width: 1024px)');
  const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');

  return (
    <div>
      {isLargeScreen && <LargeScreenFeature />}
      {!prefersReducedMotion && <Animation />}
    </div>
  );
}

Responsive Component Props

// Type-safe responsive props
type ResponsiveValue<T> = T | {
  xs?: T;
  sm?: T;
  md?: T;
  lg?: T;
  xl?: T;
  '2xl'?: T;
};

interface BoxProps {
  padding?: ResponsiveValue<number>;
  gap?: ResponsiveValue<number>;
}

function Box({ padding, gap }: BoxProps) {
  // Handle responsive values
  const getPadding = () => {
    if (typeof padding === 'number') {
      return `${padding * 4}px`;
    }
    // Generate responsive classes
    return undefined;
  };

  return <div className="box" style={{ padding: getPadding() }} />;
}

// Usage
<Box padding={4} />                    // 16px all screens
<Box padding={{ xs: 2, md: 4, lg: 6 }} /> // Responsive

Fluid Typography

/* Scales between viewport widths */
.h1 {
  font-size: clamp(
    1.875rem,                    /* Min: 30px */
    1.875rem + 1.125 * (100vw - 20rem) / 44,  /* Scale */
    3rem                         /* Max: 48px */
  );
}

/* Simpler version */
.h1 {
  font-size: clamp(1.875rem, 5vw, 3rem);
}
// Generate fluid values
function fluidSize(minPx: number, maxPx: number, minVw: number = 320, maxVw: number = 1280) {
  const minRem = minPx / 16;
  const maxRem = maxPx / 16;
  
  return `clamp(${minRem}rem, ${minRem}rem + ${maxRem - minRem} * (100vw - ${minVw}px) / ${maxVw - minVw}, ${maxRem}rem)`;
}

// Usage
const fluidH1 = fluidSize(30, 48); // 30px to 48px

Container Queries (Modern)

/* Container query tokens */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* Card adapts to container, not viewport */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

@container card (min-width: 600px) {
  .card {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Testing Responsive Designs

// Test at different breakpoints
import { render } from '@testing-library/react';

describe('ResponsiveComponent', () => {
  it('renders mobile layout', () => {
    global.innerWidth = 375;
    global.dispatchEvent(new Event('resize'));
    
    render(<ResponsiveComponent />);
    expect(screen.getByTestId('mobile-layout')).toBeInTheDocument();
  });

  it('renders desktop layout', () => {
    global.innerWidth = 1024;
    global.dispatchEvent(new Event('resize'));
    
    render(<ResponsiveComponent />);
    expect(screen.getByTestId('desktop-layout')).toBeInTheDocument();
  });
});

Best Practices

  1. Mobile-First: Start with mobile, enhance for larger screens
  2. Fluid Between Breakpoints: Use clamp() for smooth scaling
  3. Token-Based: All breakpoints from tokens
  4. Semantic Breakpoints: Name by usage, not device
  5. Test All Sizes: Don't forget tablet landscape
  6. Container Queries: Use when appropriate (modern browsers)
  7. Performance: Avoid excessive media queries
  8. Touch Targets: 44px minimum on mobile

Common Pitfalls

Desktop-first: Hard to scale down
Mobile-first approach

Too many breakpoints: Hard to maintain
3-5 key breakpoints

Device-specific: iPhone, iPad names
Generic: mobile, tablet, desktop

Hardcoded values: 768px everywhere
Reference breakpoint tokens

Responsive design tokens create consistent, maintainable responsive experiences across your entire application!

On this page