Responsive layouts with semantic container style queries
Note
A quick note re Firefox: Firefox's support for some of the @container spec is lagging, not all demos will work at time of
writing.
Using @media queries is perhaps the most common way of handling responsive layouts in web-based UIs, even if
you're not writing CSS by hand and are using something like Tailwind. Tailwind classes with prefixes such as md:
are using @media queries.
A @media query to apply styles at different screen sizes would look something like this:
content: 'Default text';
@media (min-width: 900px) {
content: 'Width is greater than 900px';
}
@media (min-width: 1200px) {
content: 'Width is greater than 1200px';
}
Demo: (try resizing your browser window)
Usually these sizes where new styles are applied are called "breakpoints". They'll typically correspond to device screen sizes, such as mobile phones, tablets, laptops, and desktop screens.
You might have many of these @media queries throughout your project.
If you wanted to adjust these breakpoint sizes or perhaps make them configurable by a client, the CSS source would need to be modified in every place where you define responsive behaviour.
CSS variables would provide a nice solution to this, however @media queries don't support CSS variables - values
provided to a query must be fixed.
--laptop-width: 500px;
/* ... */
@media (min-width: var(--laptop-width)) {
/* apply laptop specific styles */
/* these would never be applied! it's not a valid @media query... */
}
Using a preprocessor like SCSS could help here:
$laptop-width: 700px;
// ...
@media (min-width: $laptop-width) {
/* apply laptop specific styles */
}
This is helpful for a developer, however, if you were wanting to make these breakpoints configurable by clients you're likely still out of luck - it still requires preprocessing to be performed and new CSS assets generated.
Note
You can read about @media at MozDev
A quick detour - @container queries
Note
You can read about
@containers at MozDev
@container queries have been available for a little while now. They are a great feature which allows us to name
regions of our layouts. We can then use those containers for responsive behaviour based on their size, rather than the
overall size of the viewport.
They look somewhat like @media queries:
@container (width > 500px) {
/* ... */
}
It is important to note that in the hierarchy of elements, there must be a container defined above the context of the
CSS rule in which the @container query is contained.
We might have this HTML:
<div id="parent">
<div id="child"></div>
</div>
A container can be declared by using the container-name and container-type properties, or shorthand container.
Defining the #parent as a container and using a @container query within the #child might look like this:
#parent {
container-name: parent;
container-type: inline-size;
}
#child::before {
content: 'Parent width is <= 475px.';
@container parent (width > 475px) {
content: 'Parent width is > 475px.'
}
}
Demo: (try resizing your browser window)
There are a lot of useful things which a @container query can do, I would encourage you to read more about them at
MozDev.
One of those useful features is the style() query. The style() query allows a container query to match based on the
computed value of a custom property on the container.
For example, we can change text colour based on the background color of a parent container:
#parent {
container: parent / inline-size;
--background: white;
background-color: var(--background);
@media (min-width: 900px) {
--background: black;
}
}
#child {
@container style(--background: black) {
/* Text should be white when the background is black. */
color: white;
}
}
Demo: (try resizing your browser window)
Perhaps you can see where this might be heading?
Note
Despite what MozDev implies in the documentation, only custom properties are supported right now. Hopefully in the future we'll see support for more CSS properties.
Named breakpoints in vanilla CSS
So far we've worked out that we can adapt elements based on a parent container's state, and we can also be really specific by checking for the value of computed properties.
This opens the door to more semantic breakpoint handling.
Here's what we can do:
- Use a
:rootlevel CSS variable to hold a logical name for our layout's size (eg, "mobile", "laptop", "desktop") - Use
@mediaqueries to set the CSS variable to the appropriate value based on the screen's width - Declare a
@containeron our<html>element (or a suitable parent) - Use
@containerqueries to respond accordingly based on the value of our CSS variable
The :root CSS setup might look something like this:
:root {
--breakpoint: mobile;
@media screen and (min-width: 700px) {
--breakpoint: laptop;
}
@media screen and (min-width: 1200px) {
--breakpoint: desktop;
}
}
This still relies on fixed values in @media queries, but crucially centralises them in one place.
Our HTML markup might look like this:
<div id="parent">
<div id="child"></div>
</div>
And the corresponding CSS might be:
#parent {
container: breakpoint-parent / inline-size;
}
#child::before {
content: 'Default text - should not be visible.';
@container breakpoint-parent style(--breakpoint: mobile) {
content: 'You are within the mobile breakpoint size.';
}
@container breakpoint-parent style(--breakpoint: laptop) {
content: 'You are within the laptop breakpoint size.';
}
@container breakpoint-parent style(--breakpoint: desktop) {
content: 'You are within the desktop breakpoint size.';
}
}
The container inherits the --breakpoint property from :root, which allows us to use it in the @container query.
Demo: (try resizing your browser window)
This is great - we now have a single location to change our breakpoint sizes, no search-and-replace!
If this were to be client-configurable, we still can't use CSS variables in those :root .. @media queries, but that
little snippet of code is now short enough that it could be procedurally generated and inlined in the HTML. It would be
suitable to provide a default within the CSS, and output a client-configured version in the HTML if necessary.
Browser support - Firefox lagging behind for now...
Regrettably, at the time of writing, Firefox doesn't support style() for @container rules. Fingers crossed that
comes soon, but in the meantime you'll want to handle accordingly.
Can I Use indicates that there is roughly 88% support for style()
rules.
Wrapping up
CSS remains, to this day, one of my favourite languages to write. Its separation from content encourages acessibility-first thinking, and it's still pretty wild how much responsive behaviour we can express with CSS alone.