Before I go into details of this algorithm / solution, it's useful to establish some semantics.
First, there is some historical conflation of the terms "container queries" and "element queries". Depending on the solution (how someone thinks about the problem), these are often conflated to mean the same thing. There's discussion here about renaming the repo from "container queries" to "element queries" based on various conventions, etc, but I find that what's often missed when people talk about container queries vs. element queries is that people think they're talking about the same thing, when they often aren't.
To clarify:
- A container query queries an element's container in order to apply styles
- An element query queries a given element in order to apply styles to that element. (In some proposed solutions, the element is queried in order to apply styles to children.)
Someone may say, "Yes, but isn't the container always an element?" In short, no. In some proposals, yes. In this proposal, no, and for (IMO) very good reasons.
The 2019 Container Queries Proposal
Summary
This algorithm / approach has the following:
- Zero circularity
- Single-pass resolution of queries (negates the claim here that any container query algorithm must be applied multiple times)
- Minimal syntax additions
What are we querying?
This is absolutely crucial, and what makes this query algorithm fast (as fast as media queries), deterministic, and single-pass.
What we are querying is the allocated width/height of the content box that this element will be placed in.
Immediately, someone may point out that the content box eventual dimensions can be affected by children. In order for this algorithm to be successful, that's why we must query what the content box is irrespective of children. In other words, what are the dimensions if no children were present?**
** Note: this doesn't mean we would measure as if the container were truly empty; specifically, an :empty
selector would not apply during measurement if children are present, children just don't affect the allocated width/height for the purposes of this algorithm.
Why the content box?
Just to briefly address this question, the reason why we query the content box specifically vs. the parent element's width is to avoid ambiguity / surprise when switching box models. Queries always refer to the "available space" to the child element, so that you can set properties of the child based on what pixels (or other units) will actually be available. Querying the parent element's actual (or calculated) width value would strongly bind the child element to the parent's box model. Therefore, the container must be a known constant: the content box dimensions of the parent before children are rendered.
Syntax additions
:container([MQ])
- selector query of the container, using MQ Level 3 or Level 4-style queries, and can query width / height [/ inline / block] (-axes)
aw
/ ah
/ ai
/ ab
- units relating to 1% of width / height / inline-axis / block-axis (semantically similar to vw
/ vh
/ vi
/ vb
)
Examples
<div class="parent">
<div class="child"></div>
</div>
.parent {
width: 100px;
height: 200px;
}
.child {
background: red;
color: white;
}
/* Also can be written (min-width: 100px) */
.child:container(width >= 100px) {
background: blue;
}
So far, this is similar to most other container query algorithms. Where it differs is that this query is non-circular.
For example:
.parent {
min-width: 50px;
}
.child:container(width <= 50px) {
width: 200px;
}
/** Will not apply **/
.child:container(width >= 200px) {
width: 50px;
}
The .parent
class, when laying out, has only been allocated a width of 50px. Therefore the allocated width of .parent
will stay fixed at 50px unless it is allocated a new width (not sized by children). (100aw
= 50px
)
Here's another simple example, assuming the same markup
.parent {
float: left;
}
.child {
width: 200px;
height: 100px;
background: red;
}
.child:container(min-width: 200px) {
background: blue;
}
In this example, because a float "wraps" around the child, and has no defined width to calculate irrespective of children, then the allocated width/height are both 0
(zero). Meaning the container query (min-width: 200px)
will not apply, and the background of .child
will be red
.
We can make the container query apply by changing this to the following:
.parent {
float: left;
width: 200px;
height: 100px;
}
.child {
width: 100%;
height: 100%;
background: red;
}
.child:container(min-width: 200px) {
background: blue;
}
In the above example, the background of .child
will be blue
.
Other collapsing boxes
Other examples of boxes that don't have allocated width / height would be inline boxes (display: inline-box / inline-flex / inline-grid
), or using width: fit-content / min-content
. If they don't have a width
/ min-width
or height
/ min-height
value that can calculate an allocated value before the layout of children is determined, then those values will be zero.
width: auto
Note that block boxes with a width of auto will have an allocated (non-zero) value. That is, an allocated width can be determined irrespective of children.
So, given a viewport of 1024px
, and this markup:
<body>
<div class="wrapper"></div>
</body>
body {
padding: 0; margin: 0;
}
.wrapper:container(width >= 1024px) {
background: red;
}
In this case, a user agent stylesheet styles <body>
as a block with a width of auto. Therefore the wrapper's container query applies and the background will be red
.
An example with CSS Grid
Note that a container query applies to the pre-children content box of the parent. If you are slotting children into a grid, the queries of direct children of the grid will not be related to column / row slotting.
Meaning:
.parent {
width: 300px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
.child {
grid-column: 1;
}
/** Does not apply */
.child:container(width = 100px) {
background: blue;
}
/** Does apply */
.child:container(width = 300px) {
background: red;
}
The above example helps make the important distinction between element queries and container queries. We're not querying the width that will be calculated for .child
. We are strictly querying the initial content box of .parent
.
However, we could easily nest children to "query" the width of the column for some powerful layout possibilities.
Given the following:
<div class="grid">
<div class="grid-item">
<div class="child">First</div>
</div>
<div class="grid-item">
<div class="child">Second</div>
</div>
</div>
.grid {
display: grid;
width: 300px;
grid-template-columns: 2fr 1fr;
}
.child {
height: 100px;
color: white;
}
.child:container(width = 200px) {
background: red;
}
.child:container(width = 100px) {
background: blue;
}
This would give you a layout like this:
Once .grid-item
is slotted into a column, it has a calculated initial content box width that can then be queried by the .child
.
Using height
/ inline
/ block
Most of these examples have been querying width because of the way we typically layout pages, and because of default collapsing behavior. But, given a fixed (or minimum) height on a parent, it can also be queried.
.parent {
height: 100px;
}
.child:container(height >= 100px) {
background: blue;
}
We can also query the inline-axis or block-axis, depending on writing direction or layout settings.
.child:container(inline >= 100px) {
background: blue;
}
.child:container(block >= 100px) {
background: blue;
}
Page / programmatic resizing
An important thing to note is that while the initial content box dimensions of an element used for a query can't be affected by children layout, it doesn't mean it's a "fixed" value per page load. A parent element may be resized by resize of the viewport (depending on initial width / height values) or changed programmatically.
On resize, the browser must determine if a parent element has a new initial content box size. In other words, just because the element is "resized", doesn't mean the initial content box size has changed.
This is best demonstrated by example. In the following example, no amount of "resizing" the viewport / browser window will cause the container query to match.
.parent {
float: left;
}
.child {
width: 100vw;
}
.child:container(width >= 50vw) {
background: blue;
}
The initial content box width of parent is always 0
(zero), even though the .parent
element may be visibly resizing on-screen. (As an aside, this is an advantage over ResizeObserver
-based polyfills of content queries, which are subject to circularity, since they respond to any resize of the element.)
In the following example, however, resizing of the viewport will trigger matches / un-matches of the container query, and will dynamically apply those styles.
/** Assuming parent is a child of `<body>` */
.parent {
width: auto;
}
.child {
background: blue;
}
.child:container(width >= 200px) {
background: black;
}
.child:container(width >= 400px) {
background: red;
}
In the above example, note that container queries follow rules of the cascade. By default, .child
has a background of blue
. Once the available content width is 200px
or greater, the background is black
, overriding blue
. Once the available content width is 400px
or greater, the background is red
, overriding black
and blue
.
Resizing with JavaScript
If the .parent
has explicit dimensions set as inline styles with JavaScript, then the available content box width would be updated, and container queries that query the .parent
content box would be re-evaluated.
Using allocated units
2019 Container Queries are extremely powerful, but have an important companion piece which is allocated units. This allows you to easily make your CSS styles more modular / adaptive regardless of container.
Allocated units (aw
/ ah
/ ai
/ ab
) are units relating to 1% of initial container box width / height / inline-axis / block-axis, respectively.
Let's re-use our grid example.
<div class="grid">
<div class="grid-item">
<div class="child">First Child</div>
</div>
<div class="grid-item">
<div class="child">Second Child</div>
</div>
</div>
Based on how much space .child
may take, we can scale font-size to be reflective of that additional space.
.grid {
display: grid;
width: 310px;
grid-template-columns: 2fr 1fr;
grid-gap: 10px;
}
.child {
height: 100px;
padding: 10px;
color: white;
background: blue;
font-size: 15aw; /** 15% of allocated content box width */
}
This would result in:
Of course, you can use a combination of calc()
or newer min()
, max()
or clamp()
CSS functions (when available) to moderate the range / effect of allocated units.
As a result, you can define individual, reusable "modules" that adapt to defined containers.
Note that 100aw
or 100ah
etc. may resolve to 0
(zero) if the parent content box has no intrinsic dimensions.
Question to resolve: Would it be important to define a syntax for a default container width/height for a child element if a query returns a zero for either value? Or is min()
/ clamp()
sufficient? 🤔
More advanced queries
A variety of examples of queries
/** MQ4 - value range */
.a:container(400px < width < 1000px) {}
/** can target children of queries, as expected */
.a:container(width > 10em) .b {}
/** negating queries - not equal to 100px */
.a:not(:container(width = 100px)) {}
/** Joining queries */
.a:container(width > 100px):container(height > 100px) {}
/** Nesting queries */
.a:container(width > 100px) .b:container(width > 50px) {}
Feedback
Feedback welcome. There are likely to be opinions™ around various points of this proposal, such as syntax. I think the selling points are, because this query algorithm is one-way, fast, and predictable, it's something that I believe could be implemented in browsers much sooner than previous proposals, mostly because of zero circularity. Layout / queries can be determined in a single pass, as quickly as media queries are today. This makes this a potential drop-in replacement for many/most media queries, since as noted by many other, smarter people, many people use @media
queries when what they mean are container queries (the available width for my component).
That said, there are obvious use cases of using both @media
and 2019 Container Queries. I just didn't want to get too far in the weeds of creating use cases / examples, as this proposal was already quite long.