Opting in to CSS container queries for a existing design system

Although CSS container queries have been available across major browsers for about a year now, I didn’t get an opportunity to use them until recently. I didn’t feel like it was worth messing around with them until I was more certain I could use it in production. If you’re in the same boat, a likely challenge you’ll have is figuring out how to use container queries in an existing product without messing up what’s already there.

CSS container queries allow you to specify how elements should behave depending on the size of their container. You might already be familiar with media queries and changing things like padding and font size based on the viewport, but “container” refers to the element that is housing the elements you want to change. A way of visualising this might be on a shopping or e-commerce website with lots of product images and details—you might want the image of the product and its details to display differently, depending on whether the user has chosen to see rows of 3, 4, or 5 items.

Some specifics that you might want to change can include position of images, orientation of images and text, padding or spacing values, and text alignment.

You need to declare a container by using the container-type property. I won’t go into too much detail in this post, but there are three values:

  • size: base on the inline and block dimensions of the container
  • inline-size: based on the inline dimensions of the container
  • normal: not a query container for any size, but can be used to apply styles

Using size means you need to specify the height and width of the container. We found inline-size to be a better fit for the banner component, since we want it to be adaptive with the amount of text inside of the banner. (As of writing this, I haven’t properly explored the use of size but I’d imagine it might be more appropriate for something like an icon.)

What happens when you have an existing design system that doesn’t use container queries?

Take the example of a banner component. Ours was built with an icon on the left of a block of text, and the left border of the banner was a solid thick line. (See live example.)

We hadn’t taken much consideration into how this component should behave on a smaller viewport. When container queries became more widely available, we decided to use it as an opportunity. The changes we wanted to make to the banner on a smaller viewport was to centre the text, place the icon—also centred—above the text, and remove the thick line as it unnecessarily took up space.

The Banner is a pretty simple component but doesn’t appear on every page in our product, so if we updated it and something went wrong, the radius of damage wouldn’t be catastrophic.

Our goal was to:

Provide improved functionality of the component and its visual appearance in different scenarios, while keeping it straightforward for users of the design system to adopt.

We had to decide whether we would:

  1. Build the container query code into the design system itself, so the components ✨do their thing✨
  2. Use container queries outside of the design system, leaving it to the engineers who use the system, but have guidelines to ensure consistency and expectations

After a bit of testing, we decided to try and build the container queries into the design system, by applying the CSS to the outermost element of the component. We tried it on our banner component first. Unfortunately this had a nasty effect when the component was placed inside of elements that were in layouts using flexbox, and didn’t have a width specified on them:

Yuck. This was not a common case in our product, and the fix was to add width: 100% to the parent element. But after coming across a few instances, it was enough for us to rethink our approach.

It was not ideal that people update their code because of a feature we added, that essentially caused a bug.

We thought about the assumption that it’s nice to have this feature be available automatically. But was it actually beneficial, or would it cause users more problems? What if they didn’t want the container query optimisation because the page they were building just didn’t call for it?

Make container query optimisations opt-in

While testing different methods of introducing container queries on the banner, I discovered that it was not necessary to update every single component with container queries. We decided to only apply container queries to the components that would benefit from visual adaptations. That means less work for us too. 😉

Naming convention and application

With our container naming convention we went with cmds-contain-banner, cmds-contain-toast, and so on. This is how we name most other things in our system. We chose to use the specific term of “contain” so that it wouldn’t be confused with existing class names, which can be helpful when scanning code.

We applied the desired styles to each container, for example:

@container cmds-contain-banner (max-width: $cmds-banner-size-small) {
  .cmds-banner::before {
    display: none;
  }

  .cmds-banner__inner {
    flex-direction: column;
    text-align: center;
  }

  .cmds-banner__icon {
    display: flex;
    justify-content: center;
    margin-bottom: rem($cmds-g-space-small);
  }

  .cmds-banner__content {
    padding: 0;
  }
}

Following semantic versioning, we would publish this as a minor package version as it is a new feature and not a breaking change. (🤓 Given that we messed up initially, we had to publish our fix as a major version, but for other components, we’ll be publishing as a minor update, i.e. 1.2.0 to 1.3.0.)

Opting into the feature

Using the banner component as an example, this is a summary of the steps to get the container query working on where you use the banner:

  1. Update the Banner package to the version containing the container query improvements.
  2. Create an element around the banner you wish to target, or locate an existing parent element that is as close as possible to the banner (it might be an entire section of content as you might want this improvement to apply to all banners in an area).
  3. Apply your own class name (using className as I’m using React):
    <div className="c-campaign-alert"><Banner /></div>
  4. Add CSS to that class to specify the container name and type, to match the one implemented in the component package:
    .c-campaign-alert {
      container-name: cmds-contain-banner;
      container-type: inline-size;
    }
  5. Resize and mess around with the outer widths and boundaries of the component to your heart’s content. (Read: Test that it works 🧪)

Here’s the result, with the screenshot taken from our design system example that has a resize handle:

We found that this was an optimal way to implement visual improvements with container queries into an already established design system. There might be other ways—if you have other approaches, I am curious to know.

Leave a Comment