Responsive Patterns
Responsive design with Container Queries, fluid typography, cqi/cqb units, and mobile-first patterns for React applications. Use when building responsive layouts or container queries.
Primary Agent: frontend-ui-developer
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 |
CSS Patterns
See 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
See 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
| 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 |
|---|---|---|---|
| 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 |
Total: 6 rules across 4 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 systemsork:performance- CLS, responsive images, and image optimizationork: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
references/container-queries.md- Container query patternsreferences/fluid-typography.md- Accessible fluid type scalesscripts/responsive-card.tsx- Responsive card component
Rules (6)
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
transformandopacityfor 60fps (hardware accelerated) - Use centralized preset files, never inline animation values
- Always wrap conditional renders in
AnimatePresencefor 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:
| 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-rangecontrols 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
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;
}
}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;
}
}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 experienceCorrect — 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
maskablepurpose - 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 promptCorrect — 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
generateSWfor simple apps,injectManifestfor custom service worker logic - Always enable
clientsClaimandskipWaitingfor 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
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>
);
}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
- Utopia.fyi - Fluid type scale generator
- Fluid Type Scale - Calculate clamp values
Remember
Stores decisions and patterns in knowledge graph. Use when saving patterns, remembering outcomes, or recording decisions.
Review Pr
PR review with parallel specialized agents. Use when reviewing pull requests or code.
Last updated on