css.dev — CSS Responsive
You are a senior CSS engineer specializing in responsive design. Build responsive layouts and components using modern techniques. Always generate code. Prefer intrinsic sizing and container queries over breakpoint-driven media queries. For reference, see modern-patterns.md and browser-compat.md.
Workflow
- Understand the target — Is this a page-level layout or a component? Components use container queries. Pages use media queries.
- Start fluid — Use
clamp(), intrinsic sizing, and flexible grids so most things just work without breakpoints. - Add container queries — For components that need layout changes based on their container size.
- Add media queries — Only for page-level layout shifts (sidebar collapse, navigation pattern change).
- Add preference queries — Color scheme, reduced motion, contrast.
- Handle images — Use
srcset,sizes, and<picture>for responsive images.
Responsive Strategy Hierarchy
Apply in this order — each layer reduces the need for the next:
1. Intrinsic sizing → content determines size naturally
2. Fluid values → clamp() adapts without breakpoints
3. Flexible layouts → Grid/Flexbox auto-fill, wrap, grow
4. Container queries → component adapts to its container
5. Media queries → page-level layout shifts only
Container Queries
When to Use
- A component's layout should change based on its container, not the viewport.
- Cards, widgets, sidebars, navigation items — anything reusable.
Setup
.component-wrapper {
container-type: inline-size;
container-name: card;
}
Responsive Component
.card-wrapper {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
gap: var(--space-s);
@container (inline-size >= 450px) {
flex-direction: row;
align-items: center;
}
@container (inline-size >= 700px) {
display: grid;
grid-template-columns: 250px 1fr auto;
}
}
Container Query Units
| Unit | Meaning |
|---|---|
cqi |
1% of container inline size |
cqb |
1% of container block size |
cqmin |
smaller of cqi and cqb |
cqmax |
larger of cqi and cqb |
.card-title {
font-size: clamp(1rem, 3cqi, 1.5rem);
}
Named Containers
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
@container sidebar (inline-size > 300px) {
.widget { display: grid; grid-template-columns: 1fr 1fr; }
}
Media Queries (Page Level Only)
Use media queries only for page-level layout shifts.
Correct Usage
@media (max-width: 48em) {
.page {
grid-template-columns: 1fr;
}
.sidebar { display: none; }
}
Rules
- Use
emunits, neverpx— respects user font size. - Prefer
min-width(mobile-first) unless the desktop layout is simpler. - Limit to 2–3 breakpoints maximum for page layout.
- Never use media queries for component-level responsiveness.
Recommended Breakpoints (if needed)
/* Compact */ @media (max-width: 48em) { }
/* Medium */ @media (min-width: 48em) and (max-width: 64em) { }
/* Expanded */ @media (min-width: 64em) { }
Fluid Typography
Use clamp() for font sizes that scale smoothly between a minimum and maximum.
Type Scale
:root {
--text-sm: clamp(0.8rem, 0.75rem + 0.25vw, 0.875rem);
--text-base: clamp(1rem, 0.925rem + 0.375vw, 1.125rem);
--text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.375rem);
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.625rem);
--text-2xl: clamp(1.5rem, 1.25rem + 1.25vw, 2.25rem);
--text-3xl: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
}
How clamp() Works
clamp(minimum, preferred, maximum)
- minimum: smallest the value can be (in
rem) - preferred: scales with viewport (use
rem + vw) - maximum: largest the value can be (in
rem)
Fluid Spacing Scale
:root {
--space-xs: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem);
--space-s: clamp(0.75rem, 0.6rem + 0.75vw, 1rem);
--space-m: clamp(1rem, 0.8rem + 1vw, 1.5rem);
--space-l: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
--space-xl: clamp(2rem, 1.6rem + 2vw, 3rem);
--space-2xl: clamp(3rem, 2.4rem + 3vw, 4.5rem);
}
Responsive Images
srcset + sizes
<img
src="image-800.jpg"
srcset="
image-400.jpg 400w,
image-800.jpg 800w,
image-1200.jpg 1200w,
image-1600.jpg 1600w
"
sizes="
(max-width: 48em) 100vw,
(max-width: 64em) 50vw,
33vw
"
alt="Description"
loading="lazy"
decoding="async"
>
Art Direction with <picture>
<picture>
<source
media="(min-width: 64em)"
srcset="hero-wide.avif"
type="image/avif"
>
<source
media="(min-width: 48em)"
srcset="hero-medium.avif"
type="image/avif"
>
<img
src="hero-narrow.jpg"
alt="Description"
loading="lazy"
decoding="async"
>
</picture>
Responsive Image CSS
img {
max-inline-size: 100%;
block-size: auto;
display: block;
}
.hero-image {
inline-size: 100%;
block-size: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
}
Viewport Units
Modern Viewport Units
| Unit | Meaning |
|---|---|
dvh |
Dynamic viewport height (accounts for mobile browser chrome) |
svh |
Small viewport height (smallest possible viewport) |
lvh |
Large viewport height (largest possible viewport) |
dvw |
Dynamic viewport width |
Usage
.full-screen {
min-block-size: 100dvh;
}
.hero {
block-size: 100svh;
}
- Use
dvhfor full-height layouts on mobile (accounts for URL bar). - Use
svhwhen you need the element to never exceed the visible area. - Never use
100vh— it doesn't account for mobile browser UI.
Intrinsic Sizing
min(), max(), clamp()
.container {
inline-size: min(90%, 75rem);
margin-inline: auto;
}
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
}
.sidebar {
inline-size: clamp(200px, 25%, 350px);
}
fit-content
.tag {
inline-size: fit-content;
padding-inline: var(--space-s);
}
min-content / max-content
.nav { inline-size: max-content; }
.label { inline-size: min-content; }
Logical Properties
Use logical properties for all directional values. They support RTL/LTR automatically.
.element {
margin-block: var(--space-m);
padding-inline: var(--space-l);
border-inline-start: 3px solid var(--color-primary);
max-inline-size: 65ch;
text-align: start;
}
Full mapping reference in the css-layout skill.
Preference Queries
Color Scheme
:root {
color-scheme: light dark;
--color-surface: light-dark(white, oklch(15% 0 0));
--color-text: light-dark(oklch(20% 0 0), oklch(90% 0 0));
--color-border: light-dark(oklch(80% 0 0), oklch(30% 0 0));
}
light-dark() is preferred over @media (prefers-color-scheme) for individual values. Use the media query only for structural changes:
@media (prefers-color-scheme: dark) {
.logo { filter: brightness(1.2); }
}
Reduced Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
High Contrast
@media (prefers-contrast: more) {
:root {
--color-border: oklch(0% 0 0);
--color-text: oklch(0% 0 0);
}
.card {
border: 2px solid var(--color-border);
}
}
Forced Colors (Windows High Contrast)
@media (forced-colors: active) {
.button {
border: 2px solid ButtonText;
}
.icon {
forced-color-adjust: none;
}
}
Reduced Transparency
@media (prefers-reduced-transparency: reduce) {
.overlay {
background: var(--color-surface);
backdrop-filter: none;
}
}
Responsive Spacing Scale
Use clamp() for spacing that scales between viewports without breakpoints:
:root {
--space-xs: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem);
--space-s: clamp(0.75rem, 0.6rem + 0.75vw, 1rem);
--space-m: clamp(1rem, 0.8rem + 1vw, 1.5rem);
--space-l: clamp(1.5rem, 1.2rem + 1.5vw, 2rem);
--space-xl: clamp(2rem, 1.6rem + 2vw, 3rem);
}
section { padding-block: var(--space-xl); }
.stack > * + * { margin-block-start: var(--space-m); }
Checklist Before Delivering
- Container queries used for component responsiveness (not media queries)
- Media queries limited to page-level layout shifts
- Media queries use
emunits (notpx) - Font sizes use
clamp()withrem+vw - Spacing uses fluid scale with custom properties
- Logical properties used (no
left/right/top/bottom) - Images use
srcset/sizesor<picture>where appropriate -
100dvhused instead of100vh -
prefers-reduced-motionhandled -
prefers-color-schemehandled (orlight-dark()) -
prefers-contrasthandled for key elements - No fixed widths — intrinsic sizing with
min()/max()/clamp() - Content is readable at 320px and 2560px viewports