Skip to main content
OrchestKit v7.43.0 — 104 skills, 36 agents, 173 hooks · Claude Code 2.1.105+
OrchestKit
Skills

Responsive Patterns

Responsive design with Container Queries, fluid typography, cqi/cqb units, subgrid, intrinsic layouts, foldable devices, and mobile-first patterns for React applications. Use when building responsive layouts or container queries.

Reference medium

Auto-activated — this skill loads automatically when Claude detects matching context.

Responsive Patterns

Modern responsive design patterns using Container Queries, fluid typography, and mobile-first strategies for React applications (2026 best practices).

Overview

  • Building reusable components that adapt to their container
  • Implementing fluid typography that scales smoothly
  • Creating responsive layouts without media query overload
  • Building design system components for multiple contexts
  • Optimizing for variable container sizes (sidebars, modals, grids)

Core Concepts

Container Queries vs Media Queries

FeatureMedia QueriesContainer Queries
Responds toViewport sizeContainer size
Component reuseContext-dependentTruly portable
Browser supportUniversalBaseline 2023+
Use casePage layoutsComponent layouts

Modern CSS Layout

Load Read("$\{CLAUDE_SKILL_DIR\}/rules/css-subgrid.md") for CSS Subgrid patterns: nested grid alignment, card layouts with aligned titles/content/actions, two-dimensional subgrid.

Load Read("$\{CLAUDE_SKILL_DIR\}/rules/css-intrinsic-responsive.md") for intrinsically responsive layouts: auto-fit/minmax grids, clamp() for fluid everything, container queries for component logic, zero media query patterns.

Load Read("$\{CLAUDE_SKILL_DIR\}/rules/responsive-foldables.md") for foldable/multi-screen device support: env(safe-area-inset-*), viewport segment queries, dual-screen layouts, progressive enhancement.

Key patterns covered: CSS Subgrid alignment, intrinsic responsive grids (auto-fit + minmax), fluid clamp() scales, foldable device layouts, safe area insets, viewport segment queries.

CSS Patterns

Load Read("$\{CLAUDE_SKILL_DIR\}/rules/css-patterns.md") for complete CSS examples: container queries, cqi/cqb units, fluid typography with clamp(), mobile-first breakpoints, CSS Grid patterns, and scroll-queries.

Key patterns covered: Container Query basics, Container Query Units (cqi/cqb), Fluid Typography with clamp(), Container-Based Fluid Typography, Mobile-First Breakpoints, CSS Grid Responsive Patterns, Container Scroll-Queries (Chrome 126+).

React Patterns

Load Read("$\{CLAUDE_SKILL_DIR\}/rules/react-patterns.md") for complete React examples: ResponsiveCard component, Tailwind container queries, useContainerQuery hook, and responsive images.

Key patterns covered: Responsive Component with Container Queries, Tailwind CSS Container Queries, useContainerQuery Hook, Responsive Images Pattern.

Accessibility Considerations

/* IMPORTANT: Always include rem in fluid typography */
/* This ensures user font preferences are respected */

/* ❌ WRONG: Viewport-only ignores user preferences */
font-size: 5vw;

/* ✅ CORRECT: Include rem to respect user settings */
font-size: clamp(1rem, 0.5rem + 2vw, 2rem);

/* User zooming must still work */
@media (min-width: 768px) {
  /* Use em/rem, not px, for breakpoints in ideal world */
  /* (browsers still use px, but consider user zoom) */
}

Anti-Patterns (FORBIDDEN)

/* ❌ NEVER: Use only viewport units for text */
.title {
  font-size: 5vw; /* Ignores user font preferences! */
}

/* ❌ NEVER: Use cqw/cqh (use cqi/cqb instead) */
.card {
  padding: 5cqw; /* cqw = container width, not logical */
}
/* ✅ CORRECT: Use logical units */
.card {
  padding: 5cqi; /* Container inline = logical direction */
}

/* ❌ NEVER: Container queries without container-type */
@container (min-width: 400px) {
  /* Won't work without container-type on parent! */
}

/* ❌ NEVER: Desktop-first media queries */
.element {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 768px) {
  .element {
    grid-template-columns: 1fr; /* Overriding = more CSS */
  }
}

/* ❌ NEVER: Fixed pixel breakpoints for text */
@media (min-width: 768px) {
  body { font-size: 18px; } /* Use rem! */
}

/* ❌ NEVER: Over-nesting container queries */
@container a {
  @container b {
    @container c {
      /* Too complex, reconsider architecture */
    }
  }
}

Browser Support

FeatureChromeSafariFirefoxEdge
Container Size Queries105+16+110+105+
Container Style Queries111+111+
Container Scroll-State126+126+
cqi/cqb units105+16+110+105+
clamp()79+13.1+75+79+
Subgrid117+16+71+117+

Rules

Each category has individual rule files in rules/ loaded on-demand:

CategoryRuleImpactKey Pattern
Modern CSS Layoutrules/css-subgrid.mdHIGHCSS Subgrid for nested grid alignment, card layouts
Modern CSS Layoutrules/css-intrinsic-responsive.mdHIGHIntrinsic responsive layouts, auto-fit/minmax, clamp(), zero breakpoints
Modern CSS Layoutrules/responsive-foldables.mdMEDIUMFoldable devices, safe area insets, viewport segments
CSSrules/css-patterns.mdHIGHContainer queries, cqi/cqb, fluid typography, grid, scroll-queries
Reactrules/react-patterns.mdHIGHContainer query components, Tailwind, useContainerQuery, responsive images
PWArules/pwa-service-worker.mdHIGHWorkbox caching strategies, VitePWA, update management
PWArules/pwa-offline.mdHIGHOffline hooks, background sync, install prompts
Animationrules/animation-motion.mdHIGHMotion presets, AnimatePresence, View Transitions
Animationrules/animation-scroll.mdMEDIUMCSS scroll-driven animations, parallax, progressive enhancement
Touch & Mobilerules/touch-interaction.mdHIGHTouch targets (44px min), thumb zones, pinch-to-zoom, safe areas, gestures

Total: 10 rules across 6 categories

Key Decisions

DecisionOption AOption BRecommendation
Query typeMedia queriesContainer queriesContainer for components, Media for layout
Container unitscqw/cqhcqi/cqbcqi/cqb (logical, i18n-ready)
Fluid type basevw onlyrem + vwrem + vw (accessibility)
Mobile-firstYesDesktop-firstMobile-first (less CSS, progressive)
Grid patternauto-fitauto-fillauto-fit for cards, auto-fill for icons
  • design-system-starter - Building responsive design systems
  • ork:performance - CLS, responsive images, and image optimization
  • ork:i18n-date-patterns - RTL/LTR responsive considerations

Capability Details

container-queries

Keywords: @container, container-type, inline-size, container-name Solves: Component-level responsive design

fluid-typography

Keywords: clamp(), fluid, vw, rem, scale, typography Solves: Smooth font scaling without breakpoints

responsive-images

Keywords: srcset, sizes, picture, art direction Solves: Responsive images for different viewports

mobile-first-strategy

Keywords: min-width, mobile, progressive, breakpoints Solves: Efficient responsive CSS architecture

grid-flexbox-patterns

Keywords: auto-fit, auto-fill, subgrid, minmax Solves: Responsive grid and flexbox layouts

container-units

Keywords: cqi, cqb, container width, container height Solves: Sizing relative to container dimensions

References

Load on demand with Read("$\{CLAUDE_SKILL_DIR\}/references/<file>"):

FileContent
container-queries.mdContainer query patterns
fluid-typography.mdAccessible fluid type scales

Rules (10)

Use Motion library with centralized presets for 60fps animations and reduced-motion compliance — HIGH

Motion Library & View Transitions

Use Motion (Framer Motion) for React component animations and View Transitions API for page navigation with consistent presets.

Incorrect — inline animation values without presets:

// WRONG: Inconsistent animation values, no reduced-motion support
<motion.div
  initial={{ opacity: 0, y: 50, scale: 0.8 }}
  animate={{ opacity: 1, y: 0, scale: 1 }}
  transition={{ duration: 0.7, ease: "easeInOut" }}
>
  {/* Different values everywhere, no consistency */}
</motion.div>

Correct — centralized presets with AnimatePresence:

// lib/animations.ts — Single source of truth
export const fadeInUp = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -10 },
  transition: { type: 'spring', stiffness: 300, damping: 30 },
};

export const staggerContainer = {
  animate: { transition: { staggerChildren: 0.05 } },
};

export const staggerItem = {
  initial: { opacity: 0, y: 10 },
  animate: { opacity: 1, y: 0 },
};

// Usage with AnimatePresence for exit animations
import { motion, AnimatePresence } from 'motion/react';
import { staggerContainer, staggerItem, fadeInUp } from '@/lib/animations';

function AnimatedList({ items }: { items: Item[] }) {
  return (
    <AnimatePresence mode="wait">
      <motion.ul variants={staggerContainer} initial="initial" animate="animate">
        {items.map((item) => (
          <motion.li key={item.id} variants={staggerItem} layout>
            {item.name}
          </motion.li>
        ))}
      </motion.ul>
    </AnimatePresence>
  );
}

View Transitions API for page navigation:

// React Router with View Transitions
import { Link, useViewTransitionState } from 'react-router';

function ProductCard({ product }: { product: Product }) {
  const isTransitioning = useViewTransitionState(`/products/${product.id}`);
  return (
    <Link to={`/products/${product.id}`} viewTransition>
      <img
        src={product.image}
        alt={product.name}
        style={{
          viewTransitionName: isTransitioning ? 'product-image' : undefined,
        }}
      />
    </Link>
  );
}

Key rules:

  • Animate only transform and opacity for 60fps (hardware accelerated)
  • Use centralized preset files, never inline animation values
  • Always wrap conditional renders in AnimatePresence for exit animations
  • Respect prefers-reduced-motion: reduce — disable or shorten animations
  • Keep transitions under 400ms to avoid blocking user interaction
  • Use View Transitions API for page navigation, Motion for component animations

Use CSS scroll-driven animations on the compositor thread for guaranteed 60fps performance — MEDIUM

Scroll-Driven Animations

Use CSS Scroll-Driven Animations API for performant, declarative scroll-linked effects without JavaScript.

Incorrect — JavaScript scroll listener (jank-prone):

// WRONG: Scroll listeners run on main thread, cause jank
window.addEventListener('scroll', () => {
  const progress = window.scrollY / document.body.scrollHeight;
  progressBar.style.width = `${progress * 100}%`; // Layout thrashing!
  parallaxElement.style.transform = `translateY(${window.scrollY * 0.5}px)`;
});

Correct — CSS Scroll-Driven Animations (compositor thread):

/* Reading progress bar — zero JavaScript */
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: var(--color-primary);
  transform-origin: left;
  animation: grow-progress linear;
  animation-timeline: scroll(root block);
}

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* Reveal on scroll — element enters viewport */
.reveal-on-scroll {
  animation: fade-in-up linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Parallax section */
.parallax-bg {
  animation: parallax-shift linear;
  animation-timeline: scroll(root block);
}

@keyframes parallax-shift {
  from { transform: translateY(-20%); }
  to { transform: translateY(20%); }
}

Progressive enhancement (required for browser support):

/* Feature detection — fallback for unsupported browsers */
@supports (animation-timeline: view()) {
  .reveal-on-scroll {
    animation: fade-in-up linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}

/* Fallback: IntersectionObserver in JavaScript */
@supports not (animation-timeline: view()) {
  .reveal-on-scroll {
    opacity: 0;
    transform: translateY(30px);
    transition: opacity 0.5s, transform 0.5s;
  }
  .reveal-on-scroll.visible {
    opacity: 1;
    transform: translateY(0);
  }
}

Browser support:

FeatureChromeSafariFirefoxEdge
Scroll-Driven CSS115+18.4+In dev115+
scroll() function115+18.4+In dev115+
view() function115+18.4+In dev115+

Key rules:

  • CSS Scroll-Driven Animations run on compositor thread = guaranteed 60fps
  • Always use @supports (animation-timeline: view()) for progressive enhancement
  • Use scroll(root block) for page-level progress, view() for element visibility
  • animation-range controls when animation starts/ends relative to viewport
  • Never use JavaScript scroll listeners for visual effects (use CSS or IntersectionObserver)
  • Respect prefers-reduced-motion — disable parallax and motion effects

Use intrinsically responsive layouts with auto-fit, clamp, and container queries instead of media query breakpoints — HIGH

Intrinsically Responsive Layouts

Build layouts that adapt to available space without media queries. Use auto-fit/minmax() for grids, clamp() for fluid values, and container queries for component logic.

Incorrect — many media query breakpoints for component internals:

/* WRONG: Breakpoint soup for a simple card grid */
.card-grid {
  display: grid;
  grid-template-columns: 1fr;
}
@media (min-width: 480px)  { .card-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 768px)  { .card-grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .card-grid { grid-template-columns: repeat(4, 1fr); } }
@media (min-width: 1280px) { .card-grid { grid-template-columns: repeat(5, 1fr); } }

.card-title { font-size: 1rem; }
@media (min-width: 768px)  { .card-title { font-size: 1.25rem; } }
@media (min-width: 1024px) { .card-title { font-size: 1.5rem; } }

Correct — intrinsic sizing, zero breakpoints:

/* Auto-adapting grid: columns appear/disappear based on space */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: clamp(1rem, 2vw, 2rem);
}

/* Fluid typography: scales smoothly without breakpoints */
.card-title {
  font-size: clamp(1rem, 0.8rem + 1vw, 1.5rem);
}

/* Fluid spacing: padding and margins adapt to viewport */
.card {
  padding: clamp(1rem, 3vw, 2rem);
  border-radius: clamp(0.5rem, 1vw, 1rem);
}

Container queries for component-level responsiveness:

/* Component adapts to its container, not the viewport */
.widget-wrapper {
  container-type: inline-size;
  container-name: widget;
}

@container widget (min-width: 400px) {
  .widget { flex-direction: row; }
}

@container widget (max-width: 399px) {
  .widget { flex-direction: column; }
}

When viewport queries ARE appropriate:

/* Viewport queries for page-level structural changes only */
@media (min-width: 768px) {
  .page-layout {
    display: grid;
    grid-template-columns: 250px 1fr;
    /* Sidebar appears — this is page structure, not component logic */
  }
}

Fluid clamp() patterns for common properties:

:root {
  /* Fluid font scale */
  --text-sm:  clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
  --text-md:  clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  --text-lg:  clamp(1.25rem, 1rem + 1vw, 2rem);

  /* Fluid spacing scale */
  --space-sm: clamp(0.5rem, 1vw, 1rem);
  --space-md: clamp(1rem, 2vw, 2rem);
  --space-lg: clamp(1.5rem, 4vw, 4rem);

  /* Fluid gap */
  --gap:      clamp(1rem, 2vw, 2rem);
}

Key rules:

  • Use repeat(auto-fit, minmax(MIN, 1fr)) for grids — columns adapt automatically
  • Use clamp(min, preferred, max) for font-size, padding, gap, and margin
  • Reserve @media queries for page-level structure (sidebar, nav, footer layout)
  • Use @container queries for component-level responsive logic
  • Always include rem in clamp() to respect user font preferences
  • auto-fit collapses empty tracks (cards fill space); auto-fill keeps empty tracks

CSS Responsive Patterns — HIGH

CSS Responsive Patterns

1. Container Query Basics

/* Define a query container */
.card-container {
  container-type: inline-size;
  container-name: card;
}

/* Style based on container width */
@container card (min-width: 400px) {
  .card {
    display: grid;
    grid-template-columns: 200px 1fr;
  }
}

@container card (max-width: 399px) {
  .card {
    display: flex;
    flex-direction: column;
  }
}

Incorrect:

/* Using media queries for component-level responsiveness */
@media (min-width: 400px) {
  .card { display: grid; grid-template-columns: 200px 1fr; }
}

Correct:

/* Using container queries — responds to container, not viewport */
.card-container { container-type: inline-size; }
@container (min-width: 400px) {
  .card { display: grid; grid-template-columns: 200px 1fr; }
}

2. Container Query Units (cqi, cqb)

/* Use cqi (container query inline) over cqw */
.card-title {
  /* 5% of container's inline size */
  font-size: clamp(1rem, 5cqi, 2rem);
}

.card-content {
  /* Responsive padding based on container */
  padding: 2cqi;
}

/* cqb for block dimension (height-aware containers) */
.sidebar-item {
  height: 10cqb;
}

3. Fluid Typography with clamp()

/* Accessible fluid typography */
:root {
  /* Base font respects user preferences (rem) */
  --font-size-base: 1rem;

  /* Fluid scale with min/max bounds */
  --font-size-sm: clamp(0.875rem, 0.8rem + 0.25vw, 1rem);
  --font-size-md: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
  --font-size-lg: clamp(1.25rem, 1rem + 1vw, 2rem);
  --font-size-xl: clamp(1.5rem, 1rem + 2vw, 3rem);
  --font-size-2xl: clamp(2rem, 1rem + 3vw, 4rem);
}

h1 { font-size: var(--font-size-2xl); }
h2 { font-size: var(--font-size-xl); }
h3 { font-size: var(--font-size-lg); }
p { font-size: var(--font-size-md); }
small { font-size: var(--font-size-sm); }

4. Container-Based Fluid Typography

/* For component-scoped fluid text */
.widget {
  container-type: inline-size;
}

.widget-title {
  /* Fluid within container, respecting user rem */
  font-size: clamp(1rem, 0.5rem + 5cqi, 2rem);
}

.widget-body {
  font-size: clamp(0.875rem, 0.5rem + 3cqi, 1.125rem);
}

5. Mobile-First Breakpoints

/* Mobile-first: start small, add complexity */
.layout {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Tablet and up */
@media (min-width: 768px) {
  .layout {
    flex-direction: row;
  }
}

/* Desktop */
@media (min-width: 1024px) {
  .layout {
    max-width: 1200px;
    margin-inline: auto;
  }
}

6. CSS Grid Responsive Patterns

/* Auto-fit grid (fills available space) */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1.5rem;
}

/* Auto-fill grid (maintains minimum columns) */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 1rem;
}

/* Subgrid for nested alignment */
.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3;
}

7. Container Scroll-Queries (Chrome 126+)

/* Query based on scroll state */
.scroll-container {
  container-type: scroll-state;
  container-name: scroller;
}

@container scroller scroll-state(scrollable: top) {
  .scroll-indicator-top {
    opacity: 0;
  }
}

@container scroller scroll-state(scrollable: bottom) {
  .scroll-indicator-bottom {
    opacity: 0;
  }
}

Use CSS Subgrid for automatic nested grid alignment instead of duplicating grid definitions — HIGH

CSS Subgrid

Use grid-template-columns: subgrid and grid-template-rows: subgrid to inherit parent grid tracks in nested elements. Baseline 2023+ (Chrome 117, Safari 16, Firefox 71).

Incorrect — manually duplicating parent grid columns:

/* WRONG: Nested grid duplicates parent's column definition */
.parent-grid {
  display: grid;
  grid-template-columns: 200px 1fr 100px;
  gap: 1rem;
}

.child-card {
  display: grid;
  /* Fragile: must manually match parent columns */
  grid-template-columns: 200px 1fr 100px;
  grid-column: 1 / -1;
}

/* If parent columns change, every child must be updated */

Correct — subgrid inherits parent tracks automatically:

.parent-grid {
  display: grid;
  grid-template-columns: 200px 1fr 100px;
  gap: 1rem;
}

.child-card {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  /* Automatically aligns to parent's 3-column track */
}

Card layout with aligned sections (rows subgrid):

/* Cards with aligned titles, content, and actions */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  grid-auto-rows: auto;
  gap: 1.5rem;
}

.card {
  display: grid;
  grid-template-rows: subgrid;
  grid-row: span 3; /* title + content + actions */
}

.card-title   { align-self: start; }
.card-content { align-self: start; }
.card-actions { align-self: end; }

Two-dimensional subgrid (rows + columns):

.dashboard {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto 1fr auto;
  gap: 1rem;
}

.dashboard-widget {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  grid-column: span 2;
  grid-row: span 2;
}

Key rules:

  • Use subgrid instead of duplicating parent grid track definitions
  • grid-row: span N is required so the child occupies tracks to inherit
  • Subgrid works for columns, rows, or both simultaneously
  • Combine with auto-fit/minmax() on parent for responsive card grids
  • Subgrid inherits the parent's gap — override with gap on the child if needed
  • Baseline 2023+: Chrome 117+, Safari 16+, Firefox 71+, Edge 117+

Configure PWA offline support, manifest, and background sync for native-like installability — HIGH

PWA Offline & Installability

Implement offline-first patterns, background sync, install prompts, and web app manifests for native-like PWA experiences.

Incorrect — no offline fallback or install handling:

// WRONG: App crashes when offline
// No service worker fallback, no install prompt handling
// Users get browser error page instead of offline experience

Correct — offline status hook and install prompt:

// useOnlineStatus hook
export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Install prompt hook
interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function useInstallPrompt() {
  const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
  const [isInstalled, setIsInstalled] = useState(false);

  useEffect(() => {
    const handler = (e: BeforeInstallPromptEvent) => {
      e.preventDefault();
      setInstallPrompt(e);
    };
    window.addEventListener('beforeinstallprompt', handler as EventListener);
    if (window.matchMedia('(display-mode: standalone)').matches) setIsInstalled(true);
    return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
  }, []);

  const promptInstall = async () => {
    if (!installPrompt) return false;
    await installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;
    setInstallPrompt(null);
    if (outcome === 'accepted') { setIsInstalled(true); return true; }
    return false;
  };

  return { canInstall: !!installPrompt, isInstalled, promptInstall };
}

Background sync for offline form submissions:

// sw.js
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';

registerRoute(
  /\/api\/forms/,
  new NetworkOnly({
    plugins: [
      new BackgroundSyncPlugin('formQueue', {
        maxRetentionTime: 24 * 60, // 24 hours
      }),
    ],
  }),
  'POST'
);

PWA checklist:

  • Service worker registered with offline fallback
  • Web App Manifest with icons (192px + 512px maskable)
  • HTTPS enabled (required for service workers)
  • Offline page/experience works gracefully
  • Responsive design (meta viewport set)
  • Fast First Contentful Paint (< 1.8s)

Key rules:

  • Use display: "standalone" in manifest for app-like experience
  • Include both 192px and 512px icons with maskable purpose
  • Use BackgroundSyncPlugin for offline form submissions
  • Prompt install at a natural moment (not on page load)
  • Test offline behavior in Chrome DevTools Application tab

Configure service workers with Workbox 7.x caching strategies to prevent stale content — HIGH

PWA Service Worker & Workbox

Configure service workers with Workbox 7.x for reliable caching, update management, and installability.

Incorrect — service worker without update strategy:

// WRONG: No skipWaiting means users stuck on old version
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) =>
      cache.addAll(['/index.html', '/app.js', '/style.css'])
    )
  );
});
// Missing: skipWaiting, clientsClaim, no update prompt

Correct — Workbox with proper caching strategies:

import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Static assets: CacheFirst (hashed filenames = safe to cache long)
registerRoute(
  /\.(?:js|css|woff2)$/,
  new CacheFirst({
    cacheName: 'static-v1',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 365 * 24 * 60 * 60 }),
    ],
  })
);

// API calls: NetworkFirst (fresh data preferred, cached fallback)
registerRoute(
  /\/api\//,
  new NetworkFirst({
    cacheName: 'api-cache',
    networkTimeoutSeconds: 10,
    plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })],
  })
);

// Avatars/images: StaleWhileRevalidate (show cached, update in background)
registerRoute(
  /\/avatars\//,
  new StaleWhileRevalidate({ cacheName: 'avatars' })
);

VitePWA integration:

// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
        runtimeCaching: [
          { urlPattern: /^https:\/\/api\./, handler: 'NetworkFirst' },
        ],
      },
      manifest: {
        name: 'My PWA App',
        short_name: 'MyPWA',
        theme_color: '#4f46e5',
        icons: [
          { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
          { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
        ],
      },
    }),
  ],
});

Key rules:

  • Use generateSW for simple apps, injectManifest for custom service worker logic
  • Always enable clientsClaim and skipWaiting for immediate activation
  • CacheFirst for static assets, NetworkFirst for APIs, StaleWhileRevalidate for non-critical images
  • Never cache POST responses or authentication tokens
  • Include navigateFallback: '/index.html' for SPA offline support

React Responsive Patterns — HIGH

React Responsive Patterns

Incorrect:

// Hardcoded breakpoints with window.innerWidth
function Card() {
  const isMobile = window.innerWidth < 768;
  return <div className={isMobile ? "flex-col" : "flex-row"}>...</div>;
}

Correct:

// Container queries via Tailwind — no JS needed
function Card() {
  return (
    <div className="@container">
      <div className="flex flex-col @md:flex-row">...</div>
    </div>
  );
}

Responsive Component with Container Queries

import { cn } from '@/lib/utils';

interface CardProps {
  title: string;
  description: string;
  image: string;
}

export function ResponsiveCard({ title, description, image }: CardProps) {
  return (
    <div className="@container">
      <article className={cn(
        "flex flex-col gap-4",
        "@md:flex-row @md:gap-6" // Container query breakpoints
      )}>
        <img
          src={image}
          alt=""
          className="w-full @md:w-48 aspect-video object-cover rounded-lg"
        />
        <div className="flex flex-col gap-2">
          <h3 className="text-[clamp(1rem,0.5rem+3cqi,1.5rem)] font-semibold">
            {title}
          </h3>
          <p className="text-[clamp(0.875rem,0.5rem+2cqi,1rem)] text-muted-foreground">
            {description}
          </p>
        </div>
      </article>
    </div>
  );
}

Tailwind CSS Container Queries

// @container enables container query variants (@sm, @md, @lg, etc.)
<div className="@container">
  <div className="flex flex-col @lg:flex-row @xl:gap-8">
    <div className="@sm:p-4 @md:p-6 @lg:p-8">
      Content adapts to container
    </div>
  </div>
</div>

useContainerQuery Hook

import { useRef, useState, useEffect } from 'react';

function useContainerQuery(breakpoint: number) {
  const ref = useRef<HTMLDivElement>(null);
  const [isAbove, setIsAbove] = useState(false);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new ResizeObserver(([entry]) => {
      setIsAbove(entry.contentRect.width >= breakpoint);
    });

    observer.observe(element);
    return () => observer.disconnect();
  }, [breakpoint]);

  return [ref, isAbove] as const;
}

// Usage
function AdaptiveCard() {
  const [containerRef, isWide] = useContainerQuery(400);

  return (
    <div ref={containerRef}>
      {isWide ? <HorizontalLayout /> : <VerticalLayout />}
    </div>
  );
}

Responsive Images Pattern

function ResponsiveImage({
  src,
  alt,
  sizes = "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
}: {
  src: string;
  alt: string;
  sizes?: string;
}) {
  return (
    <picture>
      {/* Art direction with different crops */}
      <source
        media="(max-width: 640px)"
        srcSet={`${src}?w=640&aspect=1:1`}
      />
      <source
        media="(max-width: 1024px)"
        srcSet={`${src}?w=800&aspect=4:3`}
      />
      <img
        src={`${src}?w=1200`}
        alt={alt}
        sizes={sizes}
        loading="lazy"
        decoding="async"
        className="w-full h-auto object-cover"
      />
    </picture>
  );
}

Support foldable and multi-screen devices with safe area insets and viewport segment queries — MEDIUM

Foldable & Multi-Screen Devices

Use env(safe-area-inset-*) for notches/cutouts and viewport segment media queries for foldable-aware layouts.

Incorrect — ignoring safe areas and foldable considerations:

/* WRONG: Content hidden behind notch or fold hinge */
.app-header {
  padding: 1rem;
  /* No safe area consideration — text hidden behind notch */
}

.main-content {
  display: flex;
  /* No awareness of fold seam — content split across hinge */
}

Correct — safe area insets for notches and cutouts:

/* Respect device safe areas (notch, rounded corners, home indicator) */
.app-header {
  padding: 1rem;
  padding-top: max(1rem, env(safe-area-inset-top));
  padding-left: max(1rem, env(safe-area-inset-left));
  padding-right: max(1rem, env(safe-area-inset-right));
}

.app-footer {
  padding-bottom: max(1rem, env(safe-area-inset-bottom));
}

/* Required: viewport-fit=cover in meta tag */
/* <meta name="viewport" content="..., viewport-fit=cover"> */

Foldable dual-screen layouts with viewport segments:

/* Detect dual-screen horizontal fold (e.g., Surface Duo landscape) */
@media (horizontal-viewport-segments: 2) {
  .app-layout {
    display: grid;
    grid-template-columns:
      env(viewport-segment-width 0 0)
      calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0))
      env(viewport-segment-width 1 0);
  }

  .panel-left  { grid-column: 1; }
  .fold-gap    { grid-column: 2; } /* Hinge area — keep empty */
  .panel-right { grid-column: 3; }
}

/* Detect dual-screen vertical fold (e.g., Surface Duo portrait) */
@media (vertical-viewport-segments: 2) {
  .app-layout {
    display: grid;
    grid-template-rows:
      env(viewport-segment-height 0 0)
      calc(env(viewport-segment-top 0 1) - env(viewport-segment-bottom 0 0))
      env(viewport-segment-height 0 1);
  }

  .panel-top    { grid-row: 1; }
  .fold-gap     { grid-row: 2; }
  .panel-bottom { grid-row: 3; }
}

Progressive enhancement for foldable support:

/* Base layout — works on all devices */
.app-layout {
  display: flex;
  flex-direction: column;
}

/* Enhanced: dual-screen aware only when supported */
@media (horizontal-viewport-segments: 2) {
  .app-layout {
    display: grid;
    /* ... dual-screen grid ... */
  }
}

Testing foldable layouts:

Chrome DevTools → Device toolbar → Select "Surface Duo" or "Galaxy Fold"
- Toggle fold posture (continuous / folded)
- Test both landscape and portrait orientations
- Verify content avoids the hinge/fold seam area

Key rules:

  • Always use env(safe-area-inset-*) with max() to handle notches and cutouts
  • Add viewport-fit=cover to the viewport meta tag to enable safe area insets
  • Use @media (horizontal-viewport-segments: 2) for side-by-side foldable layouts
  • Use @media (vertical-viewport-segments: 2) for top-bottom foldable layouts
  • Keep the hinge/fold area empty — never place interactive content on the seam
  • Test on Chrome DevTools foldable emulators (Surface Duo, Galaxy Fold)
  • Use progressive enhancement — foldable styles layer on top of base layout

Touch target sizing, thumb zones, and mobile interaction principles (Fitts's Law applied) — HIGH

Touch Interaction Principles

Apply Fitts's Law and thumb-zone ergonomics to make mobile interfaces fast, reachable, and accessible.

Incorrect — blocked zoom and undersized targets:

<!-- WRONG: user-scalable=no and maximum-scale=1 are both WCAG violations -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
/* WRONG: 24px icon with no padding — too small to tap reliably */
.icon-btn { width: 24px; height: 24px; }

Correct — proper viewport and 44px minimum tap areas:

<!-- CORRECT: Never restrict zoom — users with low vision need it -->
<meta name="viewport" content="width=device-width, initial-scale=1">
/* CORRECT: visual icon 24px, tappable area expanded to 44px via padding */
.icon-btn {
  width: 24px;
  height: 24px;
  padding: 10px; /* (24 + 20) = 44px tappable */
}

/* Primary buttons: 44px min (iOS HIG) / 48dp min (Material Design) */
.btn { min-height: 44px; padding: 0.75rem 1rem; }

/* WCAG 2.5.8 AA: 24x24px allowed only with 24px spacing buffer to nearest target */
.secondary-action { min-height: 24px; min-width: 24px; margin: 12px; }

Thumb zone — bottom of screen is easy reach; top is hard:

// PRIMARY actions → bottom 1/3 (easy reach)
// SECONDARY actions → middle 1/3 (comfortable)
// STATUS / rarely-used → top 1/3 (hard reach)

// Bottom tab bar preferred over top nav on mobile
// FAB: bottom-right corner for right-handed majority
function BottomNav() {
  return (
    // Tailwind: fixed bottom bar respecting home indicator safe area
    <nav className="fixed bottom-0 inset-x-0 flex bg-background border-t border-border"
         style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
      <NavItem href="/" icon={<HomeIcon />} label="Home" />
      <NavItem href="/search" icon={<SearchIcon />} label="Search" />
    </nav>
  )
}

function FAB({ onClick }: { onClick: () => void }) {
  return (
    <button onClick={onClick}
            className="fixed bottom-6 right-6 w-14 h-14 rounded-full bg-primary text-primary-foreground shadow-lg"
            style={{ bottom: 'calc(1.5rem + env(safe-area-inset-bottom))' }}>
      <PlusIcon className="w-6 h-6" aria-hidden />
      <span className="sr-only">Create new</span>
    </button>
  )
}

Safe areas and mobile-first breakpoints:

/* Landscape: side cutouts require left/right insets */
.content-area {
  padding-left:  max(1rem, env(safe-area-inset-left));
  padding-right: max(1rem, env(safe-area-inset-right));
}

/* Mobile-first: default styles are mobile, scale up with min-width */
/* 640px (sm) → 768px (md) → 1024px (lg) → 1280px (xl) → 1536px (2xl) */
.card { display: flex; flex-direction: column; }
@media (min-width: 1024px) { .card { flex-direction: row; } }

Gestures — enhance, never require:

// CORRECT: swipe is optional; close button is always present
function BottomSheet({ onClose }: { onClose: () => void }) {
  return (
    <div role="dialog" aria-modal="true">
      <button onClick={onClose} className="min-h-11 min-w-11" aria-label="Close">
        <XIcon className="w-5 h-5" />
      </button>
      {/* Swipe-to-dismiss is progressive enhancement, not the only path */}
    </div>
  )
}

Key rules:

  • Minimum touch target: 44x44px (iOS) / 48x48dp (Material) for all primary interactions
  • WCAG 2.5.8 minimum: 24x24px when spacing buffer of 24px separates it from adjacent targets
  • Icon buttons smaller than 44px must use padding or a pseudo-element to expand the tap area
  • user-scalable=no and maximum-scale=1 in viewport meta are WCAG accessibility violations — never use them
  • Bottom 1/3 of screen = easy reach — primary actions and navigation belong here
  • Prefer bottom tab bars over top navigation on mobile; FAB goes bottom-right
  • Always pair swipe/long-press gestures with a visible button alternative
  • Use env(safe-area-inset-bottom) on bottom nav and FABs to clear the home indicator
  • Use env(safe-area-inset-left/right) in landscape to avoid side cutouts
  • Never override native pinch-to-zoom behavior in CSS or JS

References (2)

Container Queries

Container Queries Reference

Container Types

/* Size queries (width/height) */
container-type: inline-size;  /* Query inline dimension */
container-type: size;         /* Query both dimensions */
container-type: normal;       /* No containment (default) */

/* Named container */
container-name: card;

/* Shorthand */
container: card / inline-size;

Query Syntax

/* Width queries */
@container (min-width: 400px) { }
@container (max-width: 399px) { }
@container (width > 400px) { }
@container (400px <= width <= 800px) { }

/* Named container queries */
@container card (min-width: 400px) { }

/* Logical properties */
@container (min-inline-size: 400px) { }
@container (min-block-size: 300px) { }

Container Query Units

/* Inline dimension (usually width) */
cqi  /* 1% of container inline size */

/* Block dimension (usually height) */
cqb  /* 1% of container block size */

/* Min/max of cqi and cqb */
cqmin
cqmax

/* Legacy (avoid - not logical) */
cqw  /* Container width */
cqh  /* Container height */

Tailwind CSS v4 Integration

Container queries are built into Tailwind CSS v4 - no plugin required.

<!-- Enable container with @container -->
<div class="@container">
  <div class="flex flex-col @md:flex-row @lg:gap-8">
    <!-- Responsive to container, not viewport -->
  </div>
</div>

<!-- Named containers -->
<div class="@container/card">
  <div class="@lg/card:grid-cols-2">
    <!-- Queries the 'card' container -->
  </div>
</div>
/* app.css - Tailwind v4 CSS-first approach */
@import "tailwindcss";

@theme {
  /* Custom container breakpoints (optional) */
  --container-3xs: 16rem;
  --container-2xs: 18rem;
  --container-xs: 20rem;
  /* Default breakpoints: @sm (20rem), @md (28rem), @lg (32rem), etc. */
}

Best Practices

/* ✅ Use logical units */
.card-title {
  font-size: clamp(1rem, 5cqi, 2rem);
  padding: 2cqi;
}

/* ✅ Nest containers carefully */
.outer {
  container: outer / inline-size;
}
.inner {
  container: inner / inline-size;
}
@container inner (min-width: 200px) { }

/* ❌ Don't query non-container */
.no-container {
  /* No container-type set */
}
@container (min-width: 400px) {
  /* This won't work! */
}

Feature Detection

@supports (container-type: inline-size) {
  .card-container {
    container-type: inline-size;
  }
}

Fluid Typography

Fluid Typography Reference

The clamp() Formula

/* Syntax: clamp(min, preferred, max) */
font-size: clamp(1rem, 0.5rem + 2vw, 2rem);

/* Breakdown:
   min: 1rem (16px at default)
   preferred: 0.5rem + 2vw (scales with viewport)
   max: 2rem (32px at default)
*/

Accessible Fluid Scale

:root {
  /* Always include rem to respect user preferences */
  --text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
  --text-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
  --text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
  --text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.25rem);
  --text-xl: clamp(1.25rem, 1rem + 1.25vw, 1.75rem);
  --text-2xl: clamp(1.5rem, 1rem + 2.5vw, 2.5rem);
  --text-3xl: clamp(1.875rem, 1rem + 4.375vw, 3.5rem);
  --text-4xl: clamp(2.25rem, 1rem + 6.25vw, 4.5rem);
}

h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
p { font-size: var(--text-base); }
small { font-size: var(--text-sm); }

Container-Based Fluid Type

/* For component-scoped scaling */
.card {
  container-type: inline-size;
}

.card-title {
  /* Scales with card width, not viewport */
  font-size: clamp(1rem, 0.5rem + 5cqi, 1.75rem);
}

.card-body {
  font-size: clamp(0.875rem, 0.5rem + 3cqi, 1rem);
}

Calculating Fluid Values

Target: 16px at 320px viewport → 24px at 1200px viewport

Formula:
preferred = min + (max - min) × (viewport - min-viewport) / (max-viewport - min-viewport)

Step 1: Convert to vw
  (24 - 16) / (1200 - 320) = 8 / 880 = 0.909% per px
  0.909 × 100 = 0.909vw

Step 2: Calculate rem offset
  At 320px: 16px = 1rem
  16 - (320 × 0.00909) = 16 - 2.91 = 13.09px ≈ 0.818rem

Result:
font-size: clamp(1rem, 0.818rem + 0.909vw, 1.5rem);

Accessibility Considerations

/* ❌ WRONG: Ignores user font preferences */
font-size: 5vw;

/* ❌ WRONG: Completely overrides user settings */
font-size: 16px;

/* ✅ CORRECT: Respects user preferences while scaling */
font-size: clamp(1rem, 0.5rem + 2vw, 2rem);

/* The rem portion ensures user's font-size preference
   is always a factor in the final size */

Line Height Scaling

/* Tighter line-height for larger text */
h1 {
  font-size: clamp(2rem, 1rem + 5vw, 4rem);
  line-height: clamp(1.1, 1.4 - 0.2vw, 1.3);
}

p {
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
  line-height: 1.6; /* Static is fine for body */
}

Tools

Edit on GitHub

Last updated on

On this page

Responsive PatternsOverviewCore ConceptsContainer Queries vs Media QueriesModern CSS LayoutCSS PatternsReact PatternsAccessibility ConsiderationsAnti-Patterns (FORBIDDEN)Browser SupportRulesKey DecisionsRelated SkillsCapability Detailscontainer-queriesfluid-typographyresponsive-imagesmobile-first-strategygrid-flexbox-patternscontainer-unitsReferencesRules (10)Use Motion library with centralized presets for 60fps animations and reduced-motion compliance — HIGHMotion Library & View TransitionsUse CSS scroll-driven animations on the compositor thread for guaranteed 60fps performance — MEDIUMScroll-Driven AnimationsUse intrinsically responsive layouts with auto-fit, clamp, and container queries instead of media query breakpoints — HIGHIntrinsically Responsive LayoutsCSS Responsive Patterns — HIGHCSS Responsive Patterns1. Container Query Basics2. Container Query Units (cqi, cqb)3. Fluid Typography with clamp()4. Container-Based Fluid Typography5. Mobile-First Breakpoints6. CSS Grid Responsive Patterns7. Container Scroll-Queries (Chrome 126+)Use CSS Subgrid for automatic nested grid alignment instead of duplicating grid definitions — HIGHCSS SubgridConfigure PWA offline support, manifest, and background sync for native-like installability — HIGHPWA Offline & InstallabilityConfigure service workers with Workbox 7.x caching strategies to prevent stale content — HIGHPWA Service Worker & WorkboxReact Responsive Patterns — HIGHReact Responsive PatternsResponsive Component with Container QueriesTailwind CSS Container QueriesuseContainerQuery HookResponsive Images PatternSupport foldable and multi-screen devices with safe area insets and viewport segment queries — MEDIUMFoldable & Multi-Screen DevicesTouch target sizing, thumb zones, and mobile interaction principles (Fitts's Law applied) — HIGHTouch Interaction PrinciplesReferences (2)Container QueriesContainer Queries ReferenceContainer TypesQuery SyntaxContainer Query UnitsTailwind CSS v4 IntegrationBest PracticesFeature DetectionFluid TypographyFluid Typography ReferenceThe clamp() FormulaAccessible Fluid ScaleContainer-Based Fluid TypeCalculating Fluid ValuesAccessibility ConsiderationsLine Height ScalingTools