Fun fact: it’s possible to create responsive components without any media queries at all. Certainly, if we had container queries, those would be very useful for responsive design at the component level. But we don’t. Still, with or without container queries, we can do things to make our components surprisingly responsive. We’ll use concepts from Intrinsic Web Design, brought to us by Jen Simmons.
Let’s dive together into the use case described below, the solutions regarding the actual state of CSS, and some other tricks I’ll give you.
A responsive “Cooking Recipe” card
I recently tweeted a video and Pen of a responsive card demo I built using a recipe for pizza as an example. (It’s not important to the technology here, but I dropped the recipe at the end because it’s delicious and gluten free.)
Responsive Pizza Recipe Component without Media Queries.https://t.co/upft4Vpkp1
— Geoffrey Crofte 🔥 (@geoffreycrofte) July 18, 2020
Work in progress based on a design by @WalterStephanie. Have fun resizing your browser window 😊 pic.twitter.com/FHK2ghMb91
The demo here was a first attempt based on a concept from one of Stéphanie Walter’s talks. Here is a video to show you how the card will behave:
And if you want to play with it right now, here’s the Pen.
Let’s define the responsive layout
A key to planning is knowing the actual content you are working, and the importance of those details. Not that we should be hiding content at any point, but for layout and design reasons, it’s good to know what needs to be communicated first and so forth. We’ll be displaying the same content no matter the size or shape of the layout.
Let’s imagine the content with a mobile-first mindset to help us focus on what’s most important. Then when the screen is larger, like on a desktop, we can use the additional space for things like glorious whitespace and larger typography. Usually, a little prioritization like this is enough to be sure of what content is needed for the cards at any and all viewport sizes.
Let’s take the example of a cooking recipe teaser:
In her talk, Stéphanie had already did the job and prioritized the content for our cards. Here’s what she outlined, in order of importance:
- Image: because it’s a recipe, you eat with your eyes!
- Title: to be sure what you’re going to cook.
- Keywords: to catch key info at the first glance.
- Rating info: for social proof.
- Short description: for the people who read.
- Call to action: what you expect the user to do on this card.
This may seem like a lot, but we can get all of that into a single smart card layout!
Non-scalable typography
One of the constraints with the technique I’m going to show you is that you won’t be able to get scalable typography based on container width. Scalable typography (e.g. “fluid type”) is commonly done with the with viewport width (vw
) unit, which is based on the viewport, not the parent element.
So, while we might be tempted to reach for fluid type as a non-media query solution for the content in our cards, we won’t be able to use fluid type based on some percentage of the container width nor element width itself, unfortunately. That won’t stop us from our goal, however!
A quick note on “pixel perfection”
Let’s talk to both sides here…
Designers: Pixel perfect is super ideal, and we can certainly be precise at a component level. But there has to be some trade-off at the layout level. Meaning you will have to provide some variations, but allow the in-betweens to be flexible. Things shift in responsive layouts and precision at every possible screen width is a tough ask. We can still make things look great at every scale though!
Developers: You’ll have to be able to fill the gaps between the layouts that have prescribed designs to allow content to be readable and consistent between those states. As a good practice, I also recommend trying to keep as much of a natural flow as possible.
You can also read the Ahmad’s excellent article on the state of pixel perfection.
A recipe for zero media queries
Remember, what we’re striving for is not just a responsive card, but one that doesn’t rely on any media queries. It’s not that media queries should be avoided; it’s more about CSS being powerful and flexible enough for us to have other options available.
To build our responsive card, I was wondering if flexbox would be enough or if I would need to do it with CSS grid instead. Turns out flexbox in indeed enough for us this time, using the behavior and magic of the flex-wrap
and flex-basis
properties in CSS.
The gist of flex-wrap
is that it allows elements to break onto a new line when the space for content gets too tight. You can see the difference between flex with a no-wrap value and with wrapping in this demo:
The flex-basis
value of 200px is more of an instruction than a suggestion for the browser, but if the container doesn’t offer enough space for it, the elements move down onto a new line. The margin between columns even force the initial wrapping.
I used this wrapping logic to create the base of my card. Adam Argyle also used it on the following demo features four form layouts with a mere 10 lines of CSS:
In his example, Adam uses flex-basis and flex-grow (used together in flex shorthand property) )to allow the email input to take three times the space occupied by the name input or the button. When the browser estimates there is not enough rooms to display everything on the same row, the layout breaks itself into multiple lines by itself, without us having to manage the changes in media queries.
I also used clamp()
function to add even more flexibility. This function is kind of magical. It allows us to resolve a min()
and a max()
calculation in a single function. The syntax goes like this:
clamp(MIN, VALUE, MAX)
It’s like resolving a combination of the max()
and min()
functions:
max(MIN, min(VAL, MAX))
You can use it for all kind of properties that cover: <length>
, <frequency>
, <angle>
, <time>
, <percentage>
, <number>
, or <integer>
.
The “No-Media Query Responsive Card” demo
With all of these new-fangled CSS powers, I created a flexible responsive card without any media queries. It might be best to view this demo in a new tab, or with a 0.5x option in the embed below.
Something you want to note right away is that the HTML code for the 2 cards are exactly the same, the only difference is that the first card is within a 65% wide container, and the second one within a 35% wide container. You can also play with the dimension of your window to test its responsiveness.
The important part of the code in that demo is on these selectors:
.recipe
is the parent flex container..pizza-box
is a flex item that is the container for the card image..recipe-content
is a second flex item and is the container for the card content.
Now that we know how flex-wrap
works, and how flex-basis
and flex-grow
influence the element sizing, we just need to quickly explain the clamp()
function because I used it for responsive font sizing in place of where we may have normally reached for fluid type.
I wanted to use calc()
and custom properties to calculate font sizes based on the width of the parent container, but I couldn’t find a way, as a 100% value has a different interpretation depending on the context. I kept it for the middle value of my clamp()
function, but the end result was over-engineered and didn’t wind up working as I’d hoped or expected.
/* No need, really */
font-size: clamp(1.4em, calc(.5em * 2.1vw), 2.1em);
Here’s where I landed instead:
font-size: clamp(1.4em, 2.1vw, 2.1em);
That’s what I did to make the card title’s size adjust against the screen size but, like we discussed much earlier when talking about fluid type, we won’t be able to size the text by the parent container’s width.
Instead, we’re basically saying this with that one line of CSS:
I want the
font-size
to equal to 2.1vw (2.1% of the viewport width), but please don’t let it go below 1.4em or above 2.1em.
This maintains the title’s prioritized importance by allowing it to stay larger than the rest of the content, while keeping it readable. And, hey, it still makes grows and shrinks on the screen size!
And let’s not forget about responsive images, The content requirements say the image is the most important piece in the bunch, so we definitely need to account for it and make sure it looks great at all screen sizes. Now, you may want to do something like this and call it a day:
max-width: 100%;
height: auto;
But that’s doesnt always result in the best rendering of an image. Instead, we have the object-fit
property, which not only responds to the height and width of the image’s content-box, but allows us to crop the image and control how it stretches inside the box when used with the object-position
property.
img {
max-width: 100%;
min-height: 100%;
width: auto;
height: auto;
object-fit: cover;
object-position: 50% 50%;
}
As you can see, that is a lot of properties to write down. It’s mandatory because of the explicit width and height properties in the HTML <img>
code. If you remove the HTML part (which I don’t recommend for performance reason) you can keep the object-*
properties in CSS and remove the others.
An alternative recipe for no media queries
Another technique is to use flex-grow
as a unit-based growing value, with an absurdly enormous value for flex-basis
. The idea is stolen straight from the Heydon Pickering’s great “Holy Albatross” demo.
The interesting part of the code is this:
/* 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);
}
Proportional dimensions are created by flex-grow
while the flex-basis
dimension can be either invalid or extremely high. The value gets extremely high when calc(70ch - 100%)
, the value of --modifier
, reaches a positive value. When the values are extremely high each of them fills the space creating a column layout; when the values are invalid, they lay out inline.
The value of 70ch acts like the breakpoint in the recipe component (almost like a container query). Change it depending on your needs.
Let’s break down the ingredients once again
Here are the CSS ingredients we used for a media-query-less card component:
- The
clamp()
function helps resolve a “preferred” vs. “minimum” vs. “maximum” value. - The
flex-basis
property with a negative value decides when the layout breaks into multiple lines. - The
flex-grow
property is used as a unit value for proportional growth. - The
vw
unit helps with responsive typography. - The
object-fi
t property provides finer responsiveness for the card image, as it allows us to alter the dimensions of the image without distorting it.
Going further with quantity queries
I’ve got another trick for you: we can adjust the layout depending on the number of items in the container. That’s not really a responsiveness brought by the dimension of a container, but more by the context where the content lays.
There is no actual media query for number of items. It’s a little CSS trick to reverse-count the number of items and apply style modifications accordingly.
The demo uses the following selector:
.container > :nth-last-child(n+3),
.container > :nth-last-child(n+3) ~ * {
flex-direction: column;
}
Looks tricky, right? This selector allows us to apply styles from the last-child and all it’s siblings. Neat!
Una Kravets explains this concept really well. We can translate this specific usage like this:
.container > :nth-last-child(n+3)
: The third .container element or greater from the last .container in the group..container > :nth-last-child(n+3) ~ *
: The same exact thing, but selects any .container element after the last one. This helps account for any other cards we add.
Kitty Giraudel’s “Selectors Explained” tool really helps translate complex selectors into plain English, if you’d like another translation of how these selectors work.
Another way to get “quantity” containers in CSS is to use binary conditions. But the syntax is not easy and seems a bit hacky. You can reach me on Twitter if you need to talk about that — or any other tricks and tips about CSS or design.
Is this future proof?
All the techniques I presented you here can be used today in a production environment. They’re well supported and offer opportunities for graceful degradation.
Worst case scenario? Some unsupported browser, say Internet Explorer 9, won’t change the layout based on the conditions we specify, but the content will still be readable. So, it’s supported, but might not be “optimized” for the ideal experience.
Maybe one day we will finally get see the holy grail of container queries in the wild. Hopefully the Intrinsic Web Design patterns we’ve used here resonate with you and help you build flexible and “intrinsicly-responsive” components in the meantime.
Let’s get to the “rea” reason for this post… the pizza! 🍕
Gluten free pan pizza recipe
You can pick the toppings. The important part is the dough, and here is that:
Ingredients
- 3¼ cups (455g) gluten free flour
- 1 tablespoon, plus 1 teaspoon (29g) brown sugar
- 2 teaspoons of kosher salt
- 1/2 cube of yeast
- 2½ cups (400 ml) whole almond milk
- 4 tablespoons of melted margarine
- 1 tablespoon of maizena
Instructions
- Mix all the dry ingredients together.
- Add the liquids.
- Let it double size for 2 hours. I’d recommend putting a wet dish towel over your bowl where the dough is, and place the dish close to a hot area (but not too hot because we don’t want it to cook right this second).
- Put it in the pan with oil. Let it double size for approximately 1 hour.
- Cook in the oven at 250 degrees for 20 minutes.
Thanks Stéphanie for the recipe 😁
I’d just like to say, your card styling is stunning.
Hello Chris :)
Thanks for the feedback!
Thanks for the tips and the videos! I must say, I love this pizza recipe! Kudos!
Can this also be done with CSS Grid?
Love This
Thanks for the Pizza recipe
Your are very welcome
Whenever I think about this technique I can’t help but wish there was a way (in CSS) to query the visual relationship of an element to the one preceding it. So you could say if
.recipe-content
is inline with.pizza-box
do this. Or if.recipe-content
(visually) has a block relationship with.pizza-box
apply these styles. That feels quite CSSy, and would be really powerful.I can’t be more agree here Sam
…and NOT accessible. It’s not based on the user’s browsers text size, or any other custom settings. Please avoid using it.
Hello Bobby,
Thanks for your concern on accessibility! I agree the
vw
shouldn’t be used as is.That’s why I used a
clamp()
function to make it more flexible and “zoom-friendly”. I wasn’t sure, but with both max and min at aem
unit, the font is made compatible with user font-size settings. Just tested it on Chrome and Firefox with text-zoom and interface-zoom. All goodAnd what about IE. Most corporations use Windows and quite a few users and corporations still use IE10 or IE11 and this won’t work.
I have built media-query-less cards and pages, but it does me no good if it won’t work for half of the people I’m trying to reach.
It’s certainly no picnic having to produce the css to work for both and I’m still in the process of trying to get it to work.
Hello Rita,
Sorry that you still have to support IE10 and IE11. I would say that it is a corporation decision not a CSS decision here, but I won’t risk myself on that path.
You can use @supports to use the solution I propose and override a solution for IE based on floating elements. We used to adopt a technique for responsive emails with floating tables, it should work for any kind of element. Another solution is to rely on JavaScript to define a precise behavior based on the component parent.
Unfortunately there is no magic solution when you try to answer modern issues with old and unsecure tools.
Nice solution, thanks. However, I have an issue with my implementation, with styling directly transferred from your first solution – image is not fully covering its region, when I don’t specify width and height of .pizza-box as 100% – it looks like max-width and max-height are doing nothing. Do you know, what might cause this behavior?
Hello Miloš,
I can’t really answer for sure without an extract of your code, because even HTML could impact the behavior here. Indeed, if you didn’t put width and height attribute on the image, the rendering could vary.
Important note as well: I didn’t put much effort in the image ratio, you might need to tweak the CSS a little bit if the first HTML solution doesn’t work.
Have fun :)
how wrap a columns
1 2 3
4 5 6
in mobile view it should be like this
1 2
4 5
3 6
Hi Rakesh,
You should try using CSS Grid Layout for this kind of need. Look for it on CSS Tricks.