---
title: "Responsive Patterns"
description: "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."
---

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

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

<ContextualSkillSidebar slug="responsive-patterns" />


# 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

| Feature | Media Queries | Container Queries |
|---------|---------------|-------------------|
| Responds to | Viewport size | Container size |
| Component reuse | Context-dependent | Truly portable |
| Browser support | Universal | Baseline 2023+ |
| Use case | Page layouts | Component 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

```css
/* 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)

```css
/* ❌ 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

| Feature | Chrome | Safari | Firefox | Edge |
|---------|--------|--------|---------|------|
| Container Size Queries | 105+ | 16+ | 110+ | 105+ |
| Container Style Queries | 111+ | ❌ | ❌ | 111+ |
| Container Scroll-State | 126+ | ❌ | ❌ | 126+ |
| cqi/cqb units | 105+ | 16+ | 110+ | 105+ |
| clamp() | 79+ | 13.1+ | 75+ | 79+ |
| Subgrid | 117+ | 16+ | 71+ | 117+ |

## Rules

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

| Category | Rule | Impact | Key Pattern |
|----------|------|--------|-------------|
| Modern CSS Layout | `rules/css-subgrid.md` | HIGH | CSS Subgrid for nested grid alignment, card layouts |
| Modern CSS Layout | `rules/css-intrinsic-responsive.md` | HIGH | Intrinsic responsive layouts, auto-fit/minmax, clamp(), zero breakpoints |
| Modern CSS Layout | `rules/responsive-foldables.md` | MEDIUM | Foldable devices, safe area insets, viewport segments |
| CSS | `rules/css-patterns.md` | HIGH | Container queries, cqi/cqb, fluid typography, grid, scroll-queries |
| React | `rules/react-patterns.md` | HIGH | Container query components, Tailwind, useContainerQuery, responsive images |
| PWA | `rules/pwa-service-worker.md` | HIGH | Workbox caching strategies, VitePWA, update management |
| PWA | `rules/pwa-offline.md` | HIGH | Offline hooks, background sync, install prompts |
| Animation | `rules/animation-motion.md` | HIGH | Motion presets, AnimatePresence, View Transitions |
| Animation | `rules/animation-scroll.md` | MEDIUM | CSS scroll-driven animations, parallax, progressive enhancement |
| Touch & Mobile | `rules/touch-interaction.md` | HIGH | Touch targets (44px min), thumb zones, pinch-to-zoom, safe areas, gestures |

**Total: 10 rules across 6 categories**

## Key Decisions

| Decision | Option A | Option B | Recommendation |
|----------|----------|----------|----------------|
| Query type | Media queries | Container queries | **Container** for components, **Media** for layout |
| Container units | cqw/cqh | cqi/cqb | **cqi/cqb** (logical, i18n-ready) |
| Fluid type base | vw only | rem + vw | **rem + vw** (accessibility) |
| Mobile-first | Yes | Desktop-first | **Mobile-first** (less CSS, progressive) |
| Grid pattern | auto-fit | auto-fill | **auto-fit** for cards, **auto-fill** for icons |

## Related Skills

- `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/&lt;file&gt;")`:
| File | Content |
|------|---------|
| `container-queries.md` | Container query patterns |
| `fluid-typography.md` | Accessible 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:**
```tsx
// 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:**
```tsx
// 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:**
```tsx
// 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):**
```javascript
// 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):**
```css
/* 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):**
```css
/* 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:**
| Feature | Chrome | Safari | Firefox | Edge |
|---------|--------|--------|---------|------|
| Scroll-Driven CSS | 115+ | 18.4+ | In dev | 115+ |
| `scroll()` function | 115+ | 18.4+ | In dev | 115+ |
| `view()` function | 115+ | 18.4+ | In dev | 115+ |

**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:**
```css
/* 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:**
```css
/* 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:**
```css
/* 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:**
```css
/* 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:**
```css
: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

```css
/* 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:**
```css
/* Using media queries for component-level responsiveness */
@media (min-width: 400px) {
  .card { display: grid; grid-template-columns: 200px 1fr; }
}
```

**Correct:**
```css
/* 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)

```css
/* 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()

```css
/* 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

```css
/* 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

```css
/* 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

```css
/* 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+)

```css
/* 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:**
```css
/* 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:**
```css
.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):**
```css
/* 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):**
```css
.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:**
```javascript
// 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:**
```tsx
// 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:**
```javascript
// 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 (&lt; 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:**
```javascript
// 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:**
```javascript
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:**
```typescript
// 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:**
```tsx
// Hardcoded breakpoints with window.innerWidth
function Card() {
  const isMobile = window.innerWidth < 768;
  return <div className={isMobile ? "flex-col" : "flex-row"}>...</div>;
}
```

**Correct:**
```tsx
// 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

```tsx
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

```tsx
// @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

```tsx
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

```tsx
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:**
```css
/* 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:**
```css
/* 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:**
```css
/* 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:**
```css
/* 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:**
```html
<!-- 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">
```
```css
/* 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:**
```html
<!-- CORRECT: Never restrict zoom — users with low vision need it -->
<meta name="viewport" content="width=device-width, initial-scale=1">
```
```css
/* 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:**
```tsx
// 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:**
```css
/* 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:**
```tsx
// 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

```css
/* 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

```css
/* 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

```css
/* 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.

```html
<!-- 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>
```

```css
/* 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

```css
/* ✅ 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

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


### Fluid Typography

# Fluid Typography Reference

## The clamp() Formula

```css
/* 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

```css
: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

```css
/* 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

```css
/* ❌ 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

```css
/* 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

- [Utopia.fyi](https://utopia.fyi/type/calculator) - Fluid type scale generator
- [Fluid Type Scale](https://www.fluid-type-scale.com/) - Calculate clamp values

