A lot of responsive design advice still floating around predates CSS features that make the old approach (media-query-driven breakpoints for everything) unnecessary for a growing share of layout problems.
Layout stability and fluid sizing both feed directly into Core Web Vitals — a container query that reflows unexpectedly, or an image without reserved dimensions, is one of the more common real-world causes of a poor CLS score.
clamp() — fluid values without media queries
h1 {
font-size: clamp(1.5rem, 4vw + 1rem, 3rem);
}clamp(min, preferred, max) gives you a value that scales smoothly with viewport width, but never shrinks below the minimum or grows past the maximum. This replaces what used to require multiple media query breakpoints just to step font sizes up at different widths — one line, continuously fluid, no jumps.
Container queries — the fix for component-based design
The historical limitation of media queries: they respond to the viewport's size, not the size of the component itself. A card component that needs to lay out differently depending on how much space its parent container gives it (not the whole page) couldn't respond to that with media queries alone — you'd need to know exactly which page layouts it would be dropped into.
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 120px 1fr;
}
}Now the .card component switches to a side-by-side layout whenever its own container is at least 400px wide — regardless of whether that's a full-width page section or a narrow sidebar. This is what actually enables truly reusable, self-contained responsive components.
Intrinsic sizing keywords
.sidebar {
width: min(300px, 100%);
}
.gallery {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}auto-fit with minmax() creates a grid that automatically fits as many 200px-minimum columns as will fit, wrapping to a new row as needed — a responsive grid with zero media queries and zero JavaScript, driven entirely by how much space is available.
| Aspect | Media queries | Container queries |
|---|---|---|
| Responds to | The viewport's size | The component's own container size |
| Best for | Page-level layout (columns, nav visibility) | Reusable components used in varying contexts |
| Reusability | Assumes a specific page layout | Genuinely portable — same component, any container |
Where media queries are still the right tool
Container queries and fluid sizing don't replace media queries entirely — they're still the right tool for layout decisions that genuinely depend on the viewport, not a component's local context: switching a whole page from a multi-column layout to a single column, hiding a desktop-only navigation element on small screens, or adjusting overall page padding.
@media (max-width: 640px) {
.page-layout {
grid-template-columns: 1fr;
}
}Testing responsively — beyond resizing the browser
Chrome DevTools' device toolbar (Cmd/Ctrl+Shift+M) simulates specific device viewports, but real device testing still catches things simulation doesn't: actual touch target sizing, real font rendering, and genuine performance on lower-powered hardware. For anything shipping broadly, test on at least one real phone, not just a resized desktop browser window.
The practical shift in mindset
The old approach: design for a few fixed breakpoints (mobile, tablet, desktop) and hope everything in between looks acceptable. The current approach, enabled by clamp(), container queries, and intrinsic sizing: design components that respond continuously to whatever space they're actually given, with media queries reserved specifically for page-level layout decisions. It's less "pick three screen sizes to design for" and more "design something that doesn't have a size it breaks at."
Logical properties instead of physical ones
For anything that might ever need to support right-to-left languages, CSS logical properties (margin-inline-start instead of margin-left, padding-block instead of padding-top/padding-bottom) describe spacing relative to text direction rather than a fixed physical side:
.card {
margin-inline-start: 1rem; /* left in LTR, right in RTL — automatically */
padding-block: 1.5rem 2rem; /* top and bottom, in one shorthand */
}Even for a project with no current RTL requirement, padding-block/padding-inline shorthand alone (setting both matching sides in one declaration) is a small, genuine readability win over writing out padding-top/padding-bottom separately — logical properties aren't purely a future-proofing concern, they're often just less code for the same result.
The dvh unit: fixing mobile viewport height
100vh on mobile browsers has a long-standing, well-known bug: it doesn't account for the browser's own address bar showing/hiding, causing a "full height" element to actually be taller than the visible viewport and get cut off. The dvh (dynamic viewport height) unit fixes this directly:
.full-screen-hero {
height: 100dvh; /* not 100vh — accounts for mobile browser chrome */
}dvh recalculates as the browser's UI shows or hides, giving you an element that's genuinely full-viewport-height at all times, rather than one that's correct only when the address bar happens to be in whichever state it was in during the initial calculation.
:has() — a real parent selector, finally
CSS previously had no way to style a parent based on its children's state — :has() fixes this directly, and it changes what's possible without JavaScript for a real class of previously-JS-only interactions:
.form-group:has(input:invalid) {
border-color: red;
}
.card:has(img) {
grid-template-columns: 120px 1fr; /* only cards containing an image get this layout */
}The second example is a genuinely responsive-design-relevant use: a card component can lay out differently depending on whether it happens to contain an image, purely in CSS, without a JavaScript check or a manually-added modifier class — the styling adapts to the actual content structure automatically.
Common mistakes
- Setting
clamp()'s minimum too aggressively low to "be safe" — text that's technically never too small to render can still end up genuinely too small to comfortably read on an actual small device. Test the minimum on a real narrow viewport, not just in theory. - Adding
container-type: inline-sizeto a container but forgetting the container needs an actual defined width behavior (notwidth: autocollapsing to content) for the query to have anything meaningful to measure against. - Reaching for a media query to solve a problem that's actually about the component's local context, not the viewport — this is exactly the mismatch container queries exist to fix, and defaulting back to viewport-based breakpoints out of habit reintroduces the original limitation.
- Only testing in a resized desktop browser window. Simulated viewports don't reproduce real touch target sizing or font rendering differences — verify on at least one physical device before considering a responsive layout done.
Related reading
- Core Web Vitals Explained: What Actually Affects Your Score — shares tags: web-development, programming (same category).
- The JavaScript Event Loop, Explained With Diagrams — shares tags: programming, web-development.
- OWASP Top 10, Explained for Developers Who Aren't Security Specialists — shares tags: web-development, programming.
- Async Python with asyncio: A Practical Introduction — shares tags: programming.
- Big O Notation Without the Math Panic — shares tags: programming.