A Card Component That Adapts Without a Single Media Query
Responsive design at the component level has traditionally meant reaching for media queries — or waiting patiently for container queries to land. But as it turns out, flexbox and a few modern CSS functions can carry a surprising amount of the load. The techniques come from the broader idea of Intrinsic Web Design, popularized by Jen Simmons, which pushes back against rigid, viewport-driven layouts in favor of letting content and containers find their own natural rhythm.
The use case below is a recipe card that reshapes itself across screen sizes. It’s built entirely without media queries and relies on flex-wrap, flex-basis, and the clamp() function to handle the heavy lifting. The card was originally demonstrated in a video and CodePen by Geoffrey Crofte, based on a design concept from Stéphanie Walter.
Defining the Content Priorities First
Before writing any CSS, it pays to know what the card absolutely must communicate. With a mobile-first mindset, the content can be arranged in order of importance. For a recipe teaser, that means:
- Image — you eat with your eyes first.
- Title — what are we cooking?
- Keywords — key info at a glance.
- Rating — a little social proof.
- Short description — for those who read.
- Call to action — what should the user do next?
Getting all of that into a single card that remains readable at any width is the goal. But there are a couple of constraints baked into the approach worth knowing about up front.
Typography Won't Scale to the Container
This technique has one notable limitation: scalable typography based on container width isn't available. Fluid type typically uses the vw unit, which is tied to the viewport, not the parent element. So while the card's layout can flex with its container, the type will size itself against the window. That doesn't prevent a responsive feel — it just means the font sizing needs to come from a different source.
Making Peace With Imperfection
Pixel-perfect layouts at every viewport width are an unreasonable goal for designers to hand off and for developers to maintain. What's more realistic is designing a few key variations and letting the browser stretch or shrink the in-between states gracefully. For developers, the mandate is to preserve a natural content flow as much as possible so that layout shifts feel organic rather than forced.
Building the Card With Flexbox Wrapping
Flexbox turns out to be sufficient for this component — no grid required. The key player is flex-wrap, which allows flex items to drop onto a new line when they run out of room. Combine that with flex-basis, and you have a container that decides when its children should stack.
A flex-basis value of 200px is more instruction than suggestion. When the container can't offer enough space for that baseline width, the items wrap. The margins between columns even help trigger that wrapping earlier than the basis alone would.
The same pattern turns up in a form demo by Adam Argyle, where flex-basis and flex-grow are combined in the flex shorthand. That little amount of CSS allows an email input to take three times the space of a name input or a button. When the browser detects insufficient room on a single row, the elements reflow onto multiple lines without any breakpoint management.
The Magic of clamp()
clamp() adds another layer of flexibility. It resolves a minimum and maximum around a preferred value in a single function:
clamp(MIN, VALUE, MAX)
It's essentially a combination of the min() and max() functions rolled into one:
max(MIN, min(VAL, MAX))
The function applies to any CSS property that accepts lengths, frequencies, angles, times, percentages, numbers, or integers. That includes font sizes, which is exactly what makes it a handy stand-in for fluid typography.
Inside the Media-Query-Free Card Demo
The demo itself uses two cards with identical HTML. The only difference between them is the width of their parents — one sits in a container at 65% width, the other at 35%. The relevant CSS selectors are few:
.recipeis the parent flex container..pizza-boxis the flex item that wraps the card image..recipe-contentis the second flex item holding all the text content.
Where fluid type would normally step in, clamp() takes over. An initial attempt at combining calc() with custom properties to base font sizes on parent width proved over-engineered and ineffective, since a 100% value means different things depending on its context.
/* No need, really */
font-size: clamp(1.4em, calc(.5em * 2.1vw), 2.1em);
The final landing spot for the title's font-size looks like this:
font-size: clamp(1.4em, 2.1vw, 2.1em);
Translated, that line says: size the font-size at 2.1vw, but never lower than 1.4em and never higher than 2.1em. This allows the card title to maintain its importance relative to other content while remaining readable at both large and small sizes.
Responsive Images Without Distortion
Since the image carries the highest content priority, it demands the same level of attention. A naive approach — letting an image's intrinsic width and height dictate its rendered footprint — doesn't always produce the best result.
max-width: 100%;
height: auto;
The better route is the object-fit property, which controls how an image stretches and crops inside its content box. Combined with object-position, it offers granular control over how the image appears as the box changes shape.
img {
max-width: 100%;
min-height: 100%;
width: auto;
height: auto;
object-fit: cover;
object-position: 50% 50%;
}
That's a fair number of properties, but it's necessary. The explicit width and height attributes in the HTML require it for the image to behave predictably. Removing the HTML attributes — which isn't advisable for performance — would allow a leaner CSS approach with just the object-* properties intact.
A Second Technique: The Holy Albatross Pattern
An alternative approach borrows from Heydon Pickering's "Holy Albatross" demo. In this variation, flex-grow acts as a proportional growth value, while flex-basis receives an absurdly large value.
/* Container */
.recipe {
--modifier: calc(70ch - 100%);
display: flex;
flex-wrap: wrap;
}
/* Image dimension */
.pizza-box {
flex-grow: 3;
flex-shrink: 1;
flex-basis: calc(var(--modifier) * 999);
}
/* Text content dimension */
.recipe-content {
flex-grow: 4;
flex-shrink: 1;
flex-basis: calc(var(--modifier) * 999);
}
The idea is that the flex-basis value of calc(70ch - 100%) becomes either invalid or extremely high. When it reaches a positive extreme, each flex item fills the space, producing a stacked column layout. When the value is invalid, the items lay out inline. The 70ch figure effectively serves as the breakpoint — the component's own private, container-based threshold. Changing that number shifts where the layout reorganizes, almost like a preview of true container queries.
Ingredients Summary
Strip away the demo specifics, and the tech stack for a media-query-less card boils down to five tools:
clamp()resolves preferred, minimum, and maximum values in one declaration.flex-basiswith a calculated value decides when the layout breaks onto multiple lines.flex-growhandles proportional growth as a unit-based value.- The
vwunit supplies a responsive baseline for typography. object-fitmanages image sizing without the distortion that comes from direct width and height manipulation.
Adapting to Content Quantity
The container's dimensions aren't the only thing that can drive layout shifts. The number of items inside it can too. A niche selector trick called a quantity query makes that possible. Instead of a real query for item count, it uses reverse counting with :nth-last-child to apply styles from the last item backward.
.container > :nth-last-child(n+3),
.container > :nth-last-child(n+3) ~ * {
flex-direction: column;
}
That selector reads roughly as follows:
.container > :nth-last-child(n+3)selects the third-to-last.containerelement or later..container > :nth-last-child(n+3) ~ *applies the same logic forward, covering any additional cards that join the group.
It's a bit tricky to hold in your head, but tools like Kitty Giraudel's Selectors Explained can translate the selector syntax into plain English. For an even more advanced route, binary conditions on CSS custom properties can deliver similar "quantity awareness," though the syntax is notably less approachable.
Production Viability
All of these patterns are safe to use in production today. Browser support is solid, and the techniques degrade gracefully. In the worst case, an old browser like Internet Explorer 9 will simply ignore the conditional layout behavior and render the card as a readable block of content. It might not be the ideal experience, but nothing breaks.
True container queries are still on the horizon, but these intrinsic design patterns close much of the gap in the meantime. They offer a reliable, media-query-free path to genuinely responsive components.



