Tip of the week #002: Self-aware components in container queries

CSS container queries gives us the power to alter components based on the context they are used in. But what if we don't know where they're being used?

The challenge with container queries

Up until recently we could only change components or layouts based on the width of the viewport with media queries (using the width media feature). Now we finally can do stuff based on the width of a container, making changes to our CSS using container queries. It looks something like this:

Language: css
/* Before */ 
@media (min-width: 400px) {
  .component {
    ... 
  }  
}

/* After */
@container (min-width: 400px) {
  .component {
    ... 
  }  
}

However, it's not a simple as that. The big difference between media queries and container queries is that you have to tell the browser what a container is.

When you use @media (min-width: 400px) the browser knows that it has to listen for changes in the viewport's width, but with container queries it's not that straightforward.

You have to define them beforehand using the container-type property:

Language: css
.sidebar {
  container-type: inline-size;
}

.main {
  container-type: inline-size;
}

This means that if you write a container query for a component it only works inside containers that are defined, not everywhere the component is being used.

For many this is a major drawback, as you would have to know every context it could be used, and define a container for that scenario.

An example

Let's say you have a card component (I know, it's an exhausted example when talking about container queries, but it's the easiest example out there). And let's say you want to change the direction of its content based on the available horizontal space in its container:

One component, different looks based on the width of its container.
One component, different looks based on the width of its container.

You would think you could just write this:

Language: css
.card {
  flex-direction: column;
}

@container (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

This only works if the two containers (the narrow on the left and the wider on the right) are defined as containers using the aforementioned container-type property:

Language: css
.left-container,
.right-container {
  container-type: inline-size;
}

If I'm building this card for a component library, or as a block for a CMS that can be used in an unknown amount of places on a website, it's difficult to know about all the contexts it might appear in.

My card component can appear in lists, in a sidebar, in an area of CMS-generated rich text etc.

I don't want to go around defining containers all over the place either, I want my components to be self-aware of their surroundings. I just want them to know if they're in a tight spot or not, and change accordingly.

It's tempting to write * { container-type: inline-size }, but sadly this is not a good solution, both for performance reasons and for the unnecessary overhead.

Luckily there's another way.

One possible solution

A way to make the component self-aware is to

  1. create an outer layer around the component
  2. give it a name using container-name
  3. define it as a container using container-type
  4. add container queries to the component accordingly

Here's an example:

Language: html
<div class="cq-card">
    <article class="card">
      ...
    </article>
</div>
Language: css
/* prefixed with 'cq-' for recognizability in the DOM */
.cq-card {
  container-name: cardContainer;
  container-type: inline-size;
}

@container cardContainer (min-width: 400px) {
  .card {
    flex-direction: row;
  }
}

"But wait, how does this differ from other containers?", you may ask.

Well, this only works if you treat its surrounding container as part of the component. So the container would have to accompany the component wherever it's being used.

In a React component library it could look something like this:

Language: jsx
import React from 'react';
import './Card.css';

const Card = ({ title, image, description, link }) => {
  return (
    <div class="cq-card">
      <article className="card">
        <div className="card__image-container">
          <img src={image} className="card__image" />
        </div>
        <div className="card__content">
          <h2 className="card__title">{title}</h2>
          <p className="card__description">{description}</p>
          <a href={link} className="card__link">Read More</a>
        </div>
      </article>
    </div>
  );
};

export default Card;

Or in a .cshtml reusable partial in Razor Pages for a CMS like Umbraco or Optimizely:

Language: cshtml
@page
@model CardModel

<div class="cq-card">
  <article class="card">
      <div class="card__image-container">
          <img src="@Model.Image" class="card__image" />
      </div>
      <div class="card__content">
          <h2 class="card__title">@Model.Title</h2>
          <p class="card__description">@Model.Description</p>
          <a href="@Model.Link" class="card__link">Read More</a>
      </div>
  </article>
</div>

The trick here is that we force the component to listen to its own width instead of the width of the actual container it is inside of (or at least what we would think of as its container). Put the component inside a sidebar and it will fill the width of the sidebar. If the sidebar is narrow, the component becomes narrow. If the sidebar is wide, the component becomes wide. The container queries will therefore trigger when they have to.

You get the idea.


In conclusion

It can sometimes be tricky to use container queries in component-driven web development, but the solution mentioned in this article covers a lot of unknown ground. There may however be downsides to this approach, so be careful implementing it on every component without thourough testing.