PatternsDesign Systems
Responsive Design Tokens
Build responsive systems with design tokens and breakpoints
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 }} /> // ResponsiveFluid 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 48pxContainer 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
- Mobile-First: Start with mobile, enhance for larger screens
- Fluid Between Breakpoints: Use clamp() for smooth scaling
- Token-Based: All breakpoints from tokens
- Semantic Breakpoints: Name by usage, not device
- Test All Sizes: Don't forget tablet landscape
- Container Queries: Use when appropriate (modern browsers)
- Performance: Avoid excessive media queries
- 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!