Tailwind CSS responsive design and dark mode implementation patterns for 2025/2026
Provides Tailwind CSS responsive design and dark mode patterns for modern web development.
npx claudepluginhub josiahsiegel/claude-plugin-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/advanced-responsive-patterns.mdTailwind uses a mobile-first breakpoint system. With over 60% of global web traffic from mobile devices and Google's mobile-first indexing, this approach is essential.
Key Principle: Unprefixed utilities apply to ALL screen sizes. Breakpoint prefixes apply at that size AND ABOVE.
<!-- CORRECT: Mobile-first (progressive enhancement) -->
<div class="text-sm md:text-base lg:text-lg">...</div>
<!-- INCORRECT: Desktop-first thinking -->
<div class="lg:text-lg md:text-base text-sm">...</div>
| Prefix | Min Width | Typical Devices | CSS Media Query |
|---|---|---|---|
| (none) | 0px | All mobile phones | All sizes |
sm: | 640px (40rem) | Large phones, small tablets | @media (min-width: 640px) |
md: | 768px (48rem) | Tablets (portrait) | @media (min-width: 768px) |
lg: | 1024px (64rem) | Tablets (landscape), laptops | @media (min-width: 1024px) |
xl: | 1280px (80rem) | Desktops | @media (min-width: 1280px) |
2xl: | 1536px (96rem) | Large desktops | @media (min-width: 1536px) |
Common device sizes to test:
@theme {
/* Add custom breakpoints for specific content needs */
--breakpoint-xs: 20rem; /* 320px - very small devices */
--breakpoint-3xl: 100rem; /* 1600px */
--breakpoint-4xl: 120rem; /* 1920px - full HD */
/* Override existing breakpoints based on YOUR content */
--breakpoint-sm: 36rem; /* 576px - when content needs space */
--breakpoint-lg: 62rem; /* 992px - common content width */
}
Usage:
<div class="grid xs:grid-cols-2 3xl:grid-cols-6">
<!-- Custom breakpoints work like built-in ones -->
</div>
Instead of targeting devices, let your content determine breakpoints:
@theme {
/* Based on content needs, not device specs */
--breakpoint-prose: 65ch; /* Optimal reading width */
--breakpoint-content: 75rem; /* Main content max */
}
Test your design at various widths and add breakpoints where layout breaks.
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
</div>
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
Responsive Heading
</h1>
<p class="text-sm md:text-base lg:text-lg leading-relaxed">
Responsive paragraph text
</p>
<section class="py-8 md:py-12 lg:py-16 px-4 md:px-8 lg:px-12">
<div class="max-w-4xl mx-auto">
Content with responsive padding
</div>
</section>
<nav class="flex flex-col md:flex-row items-center justify-between">
<div class="hidden md:flex gap-4">
<!-- Desktop navigation -->
</div>
<button class="md:hidden">
<!-- Mobile menu button -->
</button>
</nav>
<!-- Hidden on mobile, visible on desktop -->
<div class="hidden md:block">Desktop only</div>
<!-- Visible on mobile, hidden on desktop -->
<div class="block md:hidden">Mobile only</div>
<!-- Different content per breakpoint -->
<span class="sm:hidden">XS</span>
<span class="hidden sm:inline md:hidden">SM</span>
<span class="hidden md:inline lg:hidden">MD</span>
<span class="hidden lg:inline xl:hidden">LG</span>
<span class="hidden xl:inline 2xl:hidden">XL</span>
<span class="hidden 2xl:inline">2XL</span>
Container queries enable component-level responsiveness, independent of viewport size. This is essential for reusable components in 2025.
@plugin "@tailwindcss/container-queries";
<!-- Mark parent as a query container -->
<div class="@container">
<div class="flex flex-col @md:flex-row @lg:gap-8">
<!-- Responds to container size, not viewport -->
</div>
</div>
<!-- Named containers for multiple contexts -->
<div class="@container/card">
<div class="@lg/card:grid-cols-2 grid grid-cols-1">
<!-- Responds specifically to 'card' container -->
</div>
</div>
| Class | Min-width | Use Case |
|---|---|---|
@xs | 20rem (320px) | Small widgets |
@sm | 24rem (384px) | Compact cards |
@md | 28rem (448px) | Standard cards |
@lg | 32rem (512px) | Wide cards |
@xl | 36rem (576px) | Full-width components |
@2xl | 42rem (672px) | Large containers |
@3xl | 48rem (768px) | Page sections |
| Container Queries | Viewport Queries |
|---|---|
| Reusable components | Page-level layouts |
| Cards in various contexts | Navigation bars |
| Sidebar widgets | Hero sections |
| CMS/embedded content | Full-width sections |
Target screens below a certain size:
<!-- Only on screens smaller than md (< 768px) -->
<div class="md:hidden">Small screens only</div>
<!-- Custom max-width media query -->
<div class="[@media(max-width:600px)]:text-sm">
Custom max-width
</div>
Dark mode follows the user's operating system preference using prefers-color-scheme:
@import "tailwindcss";
/* No additional configuration needed */
<div class="bg-white dark:bg-gray-900">
<h1 class="text-gray-900 dark:text-white">Title</h1>
<p class="text-gray-600 dark:text-gray-300">Content</p>
</div>
Control dark mode with a CSS class:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
<!-- Add .dark class to html or body to enable dark mode -->
<html class="dark">
<body>
<div class="bg-white dark:bg-gray-900">
Content
</div>
</body>
</html>
// Simple toggle
function toggleDarkMode() {
document.documentElement.classList.toggle('dark');
}
// With localStorage persistence
function initDarkMode() {
const isDark = localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
}
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', isDark);
}
// Initialize on page load
initDarkMode();
const themes = ['light', 'dark', 'system'];
function setTheme(theme) {
localStorage.setItem('theme', theme);
applyTheme();
}
function applyTheme() {
const theme = localStorage.getItem('theme') || 'system';
const isDark = theme === 'dark' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
}
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (localStorage.getItem('theme') === 'system') {
applyTheme();
}
});
applyTheme();
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
<html data-theme="dark">
<body>
<div class="bg-white dark:bg-gray-900">Content</div>
</body>
</html>
npm install next-themes
// app/providers.tsx
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
);
}
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// components/ThemeToggle.tsx
'use client';
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
Toggle Theme
</button>
);
}
<!-- Text colors -->
<p class="text-gray-900 dark:text-gray-100">Primary text</p>
<p class="text-gray-600 dark:text-gray-400">Secondary text</p>
<p class="text-gray-400 dark:text-gray-500">Muted text</p>
<!-- Background colors -->
<div class="bg-white dark:bg-gray-900">Page background</div>
<div class="bg-gray-50 dark:bg-gray-800">Card background</div>
<div class="bg-gray-100 dark:bg-gray-700">Elevated background</div>
<!-- Border colors -->
<div class="border border-gray-200 dark:border-gray-700">Bordered element</div>
<!-- Interactive elements -->
<button class="bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700">
Button
</button>
@theme {
/* Light mode colors (default) */
--color-bg-primary: oklch(1 0 0);
--color-bg-secondary: oklch(0.98 0 0);
--color-text-primary: oklch(0.15 0 0);
--color-text-secondary: oklch(0.4 0 0);
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-bg-primary: oklch(0.15 0 0);
--color-bg-secondary: oklch(0.2 0 0);
--color-text-primary: oklch(0.95 0 0);
--color-text-secondary: oklch(0.7 0 0);
}
}
<div class="bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]">
Semantic colors
</div>
<article class="prose dark:prose-invert">
<!-- Markdown content automatically adapts to dark mode -->
</article>
<!-- Different layouts AND colors based on screen size and theme -->
<div class="
grid grid-cols-1 md:grid-cols-2
bg-white dark:bg-gray-900
p-4 md:p-8
text-gray-900 dark:text-white
">
<div class="hidden dark:md:block">
Only visible on md+ screens in dark mode
</div>
</div>
<!-- CORRECT: Mobile-first progression -->
<div class="text-sm md:text-base lg:text-lg">
<!-- WRONG: Desktop-first thinking (more code, more bugs) -->
<div class="lg:text-lg md:text-base text-sm">
WCAG 2.2 requires 24x24px minimum, but 44x44px is recommended:
<!-- Touch-friendly button (44px minimum) -->
<button class="min-h-11 min-w-11 px-4 py-2.5">
Click me
</button>
<!-- Touch-friendly navigation link -->
<a href="#" class="block py-3 px-4 min-h-11">
Navigation Item
</a>
<!-- Adequate spacing between touch targets -->
<div class="flex gap-3">
<button class="min-h-11 px-4 py-2">Button 1</button>
<button class="min-h-11 px-4 py-2">Button 2</button>
</div>
@theme {
--text-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
--text-fluid-lg: clamp(1.25rem, 1rem + 1.25vw, 2rem);
--text-fluid-xl: clamp(1.5rem, 1rem + 2.5vw, 3rem);
}
<h1 class="text-fluid-xl font-bold">Smoothly Scaling Heading</h1>
<p class="text-fluid-base leading-relaxed">Smoothly scaling body text.</p>
@theme {
/* Instead of raw colors, use semantic names */
--color-surface: oklch(1 0 0);
--color-surface-dark: oklch(0.15 0 0);
--color-on-surface: oklch(0.1 0 0);
--color-on-surface-dark: oklch(0.95 0 0);
}
Use the debug-screens plugin during development:
npm install -D @tailwindcss/debug-screens
@plugin "@tailwindcss/debug-screens";
/* components.css */
@layer components {
.card {
@apply bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm;
}
.section {
@apply py-12 md:py-16 lg:py-24;
}
}
Ensure sufficient contrast in both light and dark modes (WCAG 2.2):
<!-- Good contrast in both modes -->
<button class="
bg-blue-600 text-white
dark:bg-blue-500 dark:text-white
hover:bg-blue-700 dark:hover:bg-blue-400
/* Focus ring for accessibility */
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
">
Action
</button>
Respect users who prefer reduced motion:
<div class="
transition-transform duration-300
hover:scale-105
motion-reduce:transition-none
motion-reduce:hover:scale-100
">
Respects motion preferences
</div>
<!-- Lazy load below-fold images -->
<img
src="image.jpg"
alt="Description"
loading="lazy"
class="w-full h-auto"
/>
<!-- Responsive srcset -->
<img
src="medium.jpg"
srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1200w"
sizes="(min-width: 1024px) 50vw, 100vw"
alt="Responsive image"
loading="lazy"
class="w-full h-auto"
/>
@utility safe-area-pb {
padding-bottom: env(safe-area-inset-bottom);
}
<!-- Bottom navigation respects device notch -->
<nav class="fixed bottom-0 inset-x-0 safe-area-pb bg-white border-t">
Navigation
</nav>
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.