How to Make a Media Query-less responsive Card Component

Avatar of Geoffrey Crofte
Geoffrey Crofte on (Updated on )

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.)

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:

  1. Image: because it’s a recipe, you eat with your eyes!
  2. Title: to be sure what you’re going to cook.
  3. Keywords: to catch key info at the first glance.
  4. Rating info: for social proof.
  5. Short description: for the people who read.
  6. 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-fit 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. pastedGraphic.png

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

  1. Mix all the dry ingredients together.
  2. Add the liquids.
  3. 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).
  4. Put it in the pan with oil. Let it double size for approximately 1 hour.
  5. Cook in the oven at 250 degrees for 20 minutes.

Thanks Stéphanie for the recipe 😁