In this comprehensive lesson, you'll master the art of creating responsive layouts and implementing powerful theming systems using Tailwind CSS and shadcn/ui. You'll learn how to build interfaces that adapt seamlessly across all devices, create flexible design systems, and implement dynamic theming that enhances user experience while maintaining performance and accessibility.
Responsive design is not just about making layouts fit different screen sizes—it's about creating optimal user experiences across all devices and contexts. The philosophy encompasses several key principles:
Progressive Enhancement: Start with a solid foundation that works everywhere, then enhance the experience for capable devices. This ensures your application is accessible to all users, regardless of their device capabilities.
Content-First Approach: Design around your content and user needs rather than device specifications. Focus on what users need to accomplish and how to best present that information across different contexts.
Performance Awareness: Responsive design impacts performance significantly. Each breakpoint, image, and layout adjustment affects load times and user experience. Optimize for the most constrained devices first.
Contextual Adaptation: Consider not just screen size, but also input method (touch vs. mouse), network conditions, and usage context (on-the-go vs. desktop).
Mobile-first design is the cornerstone of modern responsive development:
Why Mobile-First: Mobile devices have the most constraints (screen size, processing power, network). By designing for mobile first, you ensure your core functionality works everywhere, then progressively enhance for larger screens.
Implementation Strategy: Start with base styles that work on small screens, then use min-width media queries to add complexity for larger screens. This approach results in cleaner, more maintainable CSS.
Benefits: Mobile-first designs tend to be more focused, performant, and user-friendly. They force you to prioritize content and functionality, resulting in better overall user experience.
Tailwind CSS provides a thoughtful breakpoint system that aligns with common device patterns:
Default Breakpoints:
sm: 640px (small tablets and large phones)md: 768px (tablets)lg: 1024px (laptops)xl: 1280px (desktops)2xl: 1536px (large desktops)Breakpoint Philosophy: Tailwind uses mobile-first breakpoints, meaning unprefixed classes apply to all screen sizes, while prefixed classes (like md:) apply at that breakpoint and above.
Custom Breakpoints: You can customize breakpoints in your Tailwind config to match your specific audience and design requirements.
Tailwind's responsive system is built on a simple yet powerful prefix system:
Prefix Syntax: Add responsive prefixes before any utility class to apply it only at specific breakpoints and above. For example, md:text-lg applies text-lg starting at the medium breakpoint.
Stacking Behavior: Multiple responsive prefixes can be stacked on the same element to create complex responsive behaviors. The order doesn't matter—Tailwind automatically sorts them correctly.
Mobile-First Default: Classes without prefixes apply to all screen sizes, providing a solid baseline that's enhanced at larger screens.
<div className="w-full md:w-1/2 lg:w-1/3 xl:w-1/4">
<!-- This div will be full-width on mobile, half-width on tablets,
one-third on laptops, and one-quarter on desktops -->
</div>
Container Padding: Use responsive padding to create breathing room that adapts to screen size:
<div className="px-4 sm:px-6 lg:px-8">
<!-- More horizontal padding on larger screens -->
</div>
Gap and Spacing: Responsive gaps in Grid and Flexbox layouts:
<div className="grid grid-cols-1 gap-4 md:gap-6 lg:gap-8">
<!-- Tighter gaps on mobile, more spacious on desktop -->
</div>
Margin Management: Responsive margins for layout control:
<section className="mt-8 md:mt-12 lg:mt-16">
<!-- Increasing vertical spacing on larger screens -->
</section>
Fluid Typography: Create text that scales appropriately across devices:
<h1 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
<!-- Headings that grow with screen size -->
</h1>
Line Height Optimization: Adjust line height for better readability at different sizes:
<p className="text-sm leading-relaxed md:text-base md:leading-normal">
<!-- Tighter text on mobile, more comfortable on desktop -->
</p>
Responsive Font Weights: Enhance hierarchy on larger screens:
<span className="font-medium md:font-semibold">
<!-- Stronger emphasis on desktop where space allows -->
</span>
Tailwind provides comprehensive Grid utilities that work seamlessly with responsive design:
Basic Grid Setup: Create responsive grid layouts:
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Single column on mobile, two on tablet, three on desktop */}
</div>
Auto-Fit and Auto-Fill: Create flexible grids that adapt to content:
<div className="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-6">
{/* Automatically adjusts column count based on available space */}
</div>
Grid Areas: Complex layouts using named grid areas:
<div className="grid grid-cols-[1fr_300px] grid-rows-[auto_1fr_auto] gap-6">
<header className="col-span-2">Header</header>
<main className="row-span-2">Main Content</main>
<aside>Sidebar</aside>
<footer className="col-span-2">Footer</footer>
</div>
Responsive Flex Direction: Change layout direction based on screen size:
<div className="flex flex-col md:flex-row gap-4">
{/* Stacked on mobile, side-by-side on desktop */}
</div>
Justify and Align Content: Responsive alignment:
<div className="flex justify-start md:justify-center lg:justify-end">
{/* Alignment shifts based on screen size */}
</div>
Flex Wrap Control: Manage wrapping behavior:
<div className="flex flex-wrap gap-2">
{/* Items wrap naturally on smaller screens */}
</div>
Container queries represent the future of responsive design, allowing components to respond to their container size rather than the viewport:
Setup Container Queries: Configure Tailwind for container queries:
// tailwind.config.js
module.exports = {
theme: {
extend: {
containers: {
'2xl': '1400px',
},
},
},
}
Usage in Components:
<div className="@container">
<div className="@lg:text-xl @2xl:text-2xl">
{/* Text responds to container size, not viewport */}
</div>
</div>
Design tokens are the building blocks of a robust theming system:
Token Categories: Organize tokens into logical groups:
Token Naming: Use semantic names that describe purpose, not appearance:
:root {
--color-primary: 220 90% 56%;
--color-text-primary: 220 14% 96%;
--spacing-md: 1rem;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
CSS custom properties (variables) are perfect for theming:
HSL Color Values: Use HSL for easier theme manipulation:
:root {
--primary-h: 220;
--primary-s: 90%;
--primary-l: 56%;
--primary: var(--primary-h) var(--primary-s) var(--primary-l);
}
Scoped Theming: Create theme-specific variable sets:
[data-theme="light"] {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
[data-theme="dark"] {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
Component-Level Tokens: Override tokens for specific components:
.card {
--card-bg: var(--background);
--card-border: var(--border);
--card-shadow: var(--shadow-md);
}
System Preference Detection: Detect user's preferred theme:
const [theme, setTheme] = useState<'light' | 'dark'>('light')
useEffect(() => {
// Check system preference
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches
setTheme(systemPreference ? 'dark' : 'light')
// Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => {
setTheme(e.matches ? 'dark' : 'light')
}
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
Local Storage Persistence: Remember user's theme choice:
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null
if (savedTheme) {
setTheme(savedTheme)
}
}, [])
useEffect(() => {
localStorage.setItem('theme', theme)
document.documentElement.setAttribute('data-theme', theme)
}, [theme])
Theme Provider Context: Create a theme context for your application:
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
}, [theme])
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
Theme Toggle Component: Create an accessible theme switcher:
import { Button } from "@/components/ui/button"
import { Moon, Sun } from "lucide-react"
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
</Button>
)
}
Conditional Styling: Components that adapt to the current theme:
export function ThemedCard({ children }: { children: React.ReactNode }) {
return (
<div className="bg-card text-card-foreground border-border">
{children}
</div>
)
}
Theme-Specific Logic: Components with different behavior per theme:
export function ThemeAwareChart() {
const { theme } = useTheme()
const chartColors = theme === 'dark'
? ['#8b5cf6', '#3b82f6', '#10b981']
: ['#7c3aed', '#2563eb', '#059669']
return <Chart colors={chartColors} />
}
Theme Registry: Support multiple themes beyond light/dark:
const themes = {
light: { name: 'Light', colors: { /* light colors */ } },
dark: { name: 'Dark', colors: { /* dark colors */ } },
blue: { name: 'Ocean', colors: { /* blue theme */ } },
green: { name: 'Forest', colors: { /* green theme */ } },
}
export function ThemeSelector() {
const { theme, setTheme } = useTheme()
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
{Object.entries(themes).map(([key, value]) => (
<option key={key} value={key}>{value.name}</option>
))}
</select>
)
}
Dynamic Theme Loading: Load themes on demand:
async function loadTheme(themeName: string) {
const theme = await import(`./themes/${themeName}.css`)
// Apply theme styles
}
Custom Brand Colors: Allow brand customization:
:root {
--brand-h: 220;
--brand-s: 90%;
--brand-l: 56%;
}
[data-brand="accent"] {
--brand-h: 280;
--brand-s: 85%;
--brand-l: 60%;
}
Brand-Aware Components: Components that adapt to brand colors:
export function BrandButton({ variant = 'primary', children, ...props }) {
return (
<Button
className={`bg-brand text-brand-foreground hover:bg-brand/90`}
{...props}
>
{children}
</Button>
)
}
Critical CSS: Optimize above-the-fold content:
// Use Next.js dynamic imports for non-critical components
const LazyChart = dynamic(() => import('./Chart'), {
loading: () => <div className="animate-pulse bg-muted h-64 rounded" />,
ssr: false
})
Image Optimization: Responsive images with proper sizing:
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
className="w-full h-auto object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority
/>
Lazy Loading: Defer loading of off-screen content:
import { Suspense, lazy } from 'react'
const LazyComponent = lazy(() => import('./LazyComponent'))
function Page() {
return (
<div>
<CriticalContent />
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
)
}
Efficient Theme Switching: Minimize layout shifts during theme changes:
* {
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
CSS Containment: Optimize theme changes with CSS containment:
.theme-aware-component {
contain: style layout;
}
Reduced Motion: Respect user motion preferences:
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}
Touch Targets: Ensure adequate touch target sizes on mobile:
<button className="min-h-[44px] min-w-[44px] p-2">
{/* Minimum 44x44px touch target for accessibility */}
</button>
Focus Management: Maintain focus across responsive changes:
export function ResponsiveMenu() {
const [isOpen, setIsOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
menuRef.current?.focus()
}
}, [isOpen])
return (
<div ref={menuRef} className="relative">
{/* Menu content */}
</div>
)
}
Screen Reader Announcements: Announce layout changes:
useEffect(() => {
const announcement = screen.width < 768
? "Switched to mobile layout"
: "Switched to desktop layout"
const announcer = document.getElementById('announcer')
if (announcer) {
announcer.textContent = announcement
}
}, [screenWidth])
Contrast Ratios: Ensure sufficient contrast in all themes:
:root {
--text-primary: 222.2 84% 4.9%; /* High contrast for light theme */
}
[data-theme="dark"] {
--text-primary: 210 40% 98%; /* High contrast for dark theme */
}
Color Blindness: Design for color vision deficiencies:
export function StatusIndicator({ status }: { status: 'success' | 'error' | 'warning' }) {
const baseClasses = "w-3 h-3 rounded-full"
const statusClasses = {
success: "bg-green-500",
error: "bg-red-500",
warning: "bg-yellow-500"
}
return (
<div className={`${baseClasses} ${statusClasses[status]}`} role="img">
<span className="sr-only">{status}</span>
</div>
)
}
Browser DevTools: Use responsive design mode effectively:
Automated Testing: Visual regression testing for responsive layouts:
import { render, screen } from '@testing-library/react'
import { ThemeProvider } from '@/components/theme-provider'
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider>
{component}
</ThemeProvider>
)
}
test('component adapts to different screen sizes', () => {
renderWithTheme(<ResponsiveComponent />)
// Test mobile layout
expect(screen.getByTestId('mobile-layout')).toBeInTheDocument()
// Simulate desktop
window.innerWidth = 1024
window.dispatchEvent(new Event('resize'))
expect(screen.getByTestId('desktop-layout')).toBeInTheDocument()
})
Theme Coverage: Test all theme variations:
const themes = ['light', 'dark', 'blue', 'green'] as const
themes.forEach(theme => {
test(`component renders correctly in ${theme} theme`, () => {
document.documentElement.setAttribute('data-theme', theme)
renderWithTheme(<ThemedComponent />)
// Verify theme-specific styles
expect(screen.getByRole('button')).toHaveClass(`bg-${theme}-primary`)
})
})
Holy Grail Layout: Classic web layout pattern:
export function HolyGrailLayout() {
return (
<div className="min-h-screen flex flex-col">
<header className="bg-primary text-primary-foreground p-4">
Header
</header>
<div className="flex flex-1">
<nav className="w-64 bg-secondary p-4 hidden lg:block">
Navigation
</nav>
<main className="flex-1 p-4">
Main Content
</main>
<aside className="w-64 bg-muted p-4 hidden xl:block">
Sidebar
</aside>
</div>
<footer className="bg-border p-4">
Footer
</footer>
</div>
)
}
Card Grid Layout: Responsive card grid:
export function CardGrid({ cards }: { cards: CardData[] }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{cards.map(card => (
<Card key={card.id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<CardTitle>{card.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{card.description}</p>
</CardContent>
</Card>
))}
</div>
)
}
Theme-Aware Icons: Icons that adapt to theme:
export function ThemeAwareIcon({ icon: Icon }: { icon: React.ComponentType<any> }) {
return (
<Icon className="text-foreground" />
)
}
Conditional Components: Components that change based on theme:
export function ThemeGreeting() {
const { theme } = useTheme()
return (
<div className="text-center py-8">
<h1 className="text-4xl font-bold mb-4">
{theme === 'dark' ? '🌙 Good Evening' : '☀️ Good Morning'}
</h1>
<p className="text-muted-foreground">
Welcome to your {theme} theme experience
</p>
</div>
)
}
In this comprehensive lesson, you've mastered responsive design and theming systems using Tailwind CSS and shadcn/ui. You now understand:
In the final lesson of this module, you'll learn about accessibility and design patterns, building on the responsive and theming knowledge to create truly inclusive and maintainable user interfaces.