The idea behind container queries is seemingly simple: instead of having a media query that targets the whole viewport, target a single container element instead.
The simplicity of this idea is deceiving. While it seems simple for a case where you have a container with a set width, in CSS you're not limited to a condition like that. Instead, you'd have to take care of cases such as container element's size being determined by it's children. Which means that you can easily create infinite loops, circularity where child's size is adjusted by parent's size which is adjusted by child's size which is adjusted again by parent's size and so forth.
So far this problem has not been solved and thus we have no CSS standard and you can't find container queries over at Can I use despite having numerous JS libraries tackling the issue and even large and detailed proposals.
@media screen and (max-width: 499px) {
.element { /* styles in mobile */ }
}
@media screen and (min-width: 500px) and (max-width: 999px) {
.element { /* styles in tablet */ }
}
Then, why do we need container queries? It is likely that even 90% of use cases where a media query is now used would be better solved by a container query. A common issue with media query is that adding anything extra to the view, such as a sidebar, can cause a mismatch of earlier media query rules and you have to override the previous rules by adding some kind of indication that "hey, we have a sidebar of width X, increase used widths in media queries by X so that our element looks pretty when alongside the sidebar".
And working with that kind of logic in CSS is awful!
/* one way to solve the issue, using SCSS for some sanity... */
@media screen and (max-width: 499px) {
.container[data-sidebar="off"] > .element { /* styles in mobile */ }
}
@media screen and (max-width: #{499px + $sidebarMobileWidth}) {
.container[data-sidebar="on"] > .element { /* styles in mobile */ }
}
@media screen and (min-width: 500px) and (max-width: 999px) {
.container[data-sidebar="off"] > .element { /* styles in tablet */ }
}
@media screen and (min-width: #{500px + $sidebarTabletWidth}) and (max-width: #{999px + $sidebarTabletWidth}) {
.container[data-sidebar="on"] > .element { /* styles in tablet */ }
}
Now imagine if sidebar also has fluid width and some min-width
rules in addition... or if you had far more breakpoints where deeper child elements adjusted their size as more space becomes available!
With container queries we wouldn't have this issue as the element sizing would be based on a container that would otherwise follow regular CSS rules in its own sizing. No need for workarounds via element attributes and no duplicated rules in CSS.
Do-It-Yourself Container Queries in JavaScript
As far as standards go we haven't got anything besides media queries to work with in CSS, however JavaScript world is a different story. A recent development has been ResizeObserver API which has support in Chrome, Firefox and Samsung Internet and there is a polyfill available for other browsers.
ResizeObserver is not the only way! There has been a hack that allows detecting resize events from an empty child page that has been sized via CSS to match the size of a container element. The idea is to have the container element with position
other than static
and then size a child <object data="about:blank" type="text/html" />
via position: absolute
to be equal in size of it's parent. To make it invisible we can use clip: rect(0 0 0 0)
. The great part of this method is huge browser support as you don't need to worry about polyfilling anything.
Finally, the most typical implementation has been to listen for window resize events. This is not a perfect solution though as elements can resize even with no change in viewport size. This has been mostly used because there has been no knowledge of an alternative.
Let's go through how you can do it yourself with the two more viable options! And if you're not working with React, don't worry: there is information below that is valuable even without React-knowledge and we'll go through all the other non-DIY options as well! :)
DIY: ResizeObserver API
The first thing I want to point about this option is that always, when possible, you should use only one instance. In React world it seems fairly typical for people to create fully self-contained components, meaning that each component instance also creates all other things it uses. For performance reasons it is better to have as few ResizeObserver instances as possible!
componentDidMount() {
// no re-use :(
this.observer = new ResizeObserver(this.resize)
this.observer.observe(this.element)
}
componentWillUnmount() {
this.observer.disconnect()
}
// or in hooks
useEffect(() => {
if (!element) return
// no re-use :(
const observer = new ResizeObserver(onResize)
observer.observe(element)
return () => {
observer.disconnect()
}
}, [element, onResize])
Instead you should create a single listener that is able to call related callbacks. This is easily achievable using WeakMap
!
const callbackMap = new WeakMap()
function manageCallbacks(entries) {
for (let entry of entries) {
const callback = callbackMap.get(entry.target)
if (callback) callback(entry.contentRect)
}
}
// Babel changes `global` to `window` for client-side code
const observer = 'ResizeObserver' in global && new ResizeObserver(manageCallbacks)
// ... in component, assumes it is impossible for `this.element` reference to change
componentDidMount() {
callbackMap.set(this.element, this.resize)
observer.observe(this.element)
}
componentWillUnmount() {
observer.unobserve(this.element)
callbackMap.delete(this.element)
}
// probably a safer way to go, iirc React calls `ref` functions with `null` on unmount
getRef(el) {
if (this.el === el) return
if (this.el) {
observer.unobserve(this.el)
callbackMap.delete(this.el)
}
if (el) {
callbackMap.set(el, this.resize)
observer.observe(el)
}
this.el = el
}
The latter is also better option in that this.resize
handler will receive a contentRect
that has .width
and .height
directly available.
While the above is rather React-centric, I hope non-React devs do catch the API itself!
DIY: about:blank page inside object/iframe
With this method there are a couple of gotchas that one must be aware of, as this is a hack:
- Parent container must have
position
other thanstatic
. -
<object />
element must be hidden visually AND interactively. -
<object />
will mess up with some CSS by existing within the container, most likely:first-child
or:last-child
. - Container should not have border or padding.
Taking all of the above into account the final CSS and HTML needed would look like this:
/* use clip, pointer-events and user-select to remove visibility and interaction */
object[data="about:blank"] {
clip: rect(0 0 0 0);
height: 100%;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
user-select: none;
width: 100%;
}
<div style="position:relative">
<object aria-hidden="true" data="about:blank" tabindex="-1" type="text/html"></object>
<!-- here would be the elements that would be sized according to container -->
</div>
But it has to be noted it doesn't make much sense to serve this kind of client-only logic in HTML render, thus adding <object />
only in the browser via JavaScript makes much more sense than serving it in HTML. The biggest problem is that we need to wait for object.onload
to trigger. The code for it:
object.onload = function() {
const object = this
function complete() {
// wait for contentDocument to become available if not immediately here
if (!object.contentDocument) setTimeout(complete, 50)
else setElement(object.contentDocument.defaultView)
}
complete()
}
Here setElement
would be a function which receives the element that you can listen to for resize events by using addEventListener
. Most of the rest is all regular DOM manipulation with document.createElement
and the like :)
How about no DIY?
Like for everything in the JavaScript world, there are a lot of solutions to go with on npm
! The following list first puts focus on React-only solutions, after which you can find some solutions that work by extending CSS (with the help of JS, of course).
react-sizeme (8.2 kB minzipped)
This appears to be the most popular element size detection component out there. While quite performant, it's size is a weakness: 8 kB is a lot of stuff! And it still only gives you the size of the element: you still have to add your own logic if you want to set element className
based on your breakpoints, for example.
react-measure (3.9 kB minzipped)
The next in popularity we can find react-measure
which uses ResizeObserver
. It provides more than just width and height, allowing you to get all the measurements of an element you might need. It's own size is also half compared to react-sizeme
.
Other ResizeObserver based solutions
These React hooks are not popular, but both are minimalistic. react-element-size
only focuses on providing width and height, nothing more. react-use-size
provides a few more features.
Core weakness regarding their total size is the forced inclusion of a polyfill, although this is not unique to these hooks. It would be better if the polyfill wouldn't be included and be delegated as user developer's problem, as people might use service like polyfill.io
to optimize the delivery of their polyfills. This is a case where library authors should forget about developer friendliness on a matter and just instruct devs to include polyfill whichever way suits them best, and not force a polyfill.
Another problem these hooks have is that they do not re-use ResizeObserver
, instead making a new observer instance for each tracked element.
react-resize-aware (0.61 kB minzipped)
This tiny hook uses <iframe />
with about:blank
and thus adds extra element into the HTML, forcing to include position: relative
or equivalent style to a container element. Besides that it does just what is needed to provide width and height information. This is a very good option if you don't mind calculating matches to breakpoints on your own!
styled-container-query (5.6 kB minzipped)
As the first true Container Queries solution on the list we find an extension for Styled Components. This means you get a JS-in-CSS solution with :container
pseudo selectors and you're allowed to write with no boilerplate!
As of writing this the downside of this library is that it has some performance issues, but I brought them up and I hope the library author gets them sorted out :)
Also, using objects and props
callback support are not supported which takes a bit away from the usefulness of this solution. If you have knowledge about Styled Components and have time to help I'd suggest to go ahead and improve this one as the idea is great!
react-use-queries (0.75 kB minzipped)
Similar to react-resize-aware
this hook has the same weakness of adding extra listener element to the DOM. The main difference between these utilities is that instead of width and height you can give a list of media queries. You can also match anything for output, not just strings, having a lot of power especially if you want or need to do more than just classNames.
As an advantage over react-resize-aware
you have far less events triggering as react-use-queries
makes use of matchMedia
listeners instead of a resize event.
As final note: this one is by me :)
Non-React "write as CSS" solutions
-
Mark J. Schmidt:
First released in 2014, widely browser compatible utility that is based on
min-width
andmax-width
attribute selectors. -
Martin Auswöger:
CQ Prolyfill (container queries speculative polyfill)
First released in 2015, uses
:container
pseudo selector. -
Tommy Hodgins:
First released in 2017, has a very complete spec, but has verbose
@element
syntax. -
Viktor Hubert:
First released in 2017, a PostCSS plugin and JS runtimes using
@container
SCSS-syntax.
I'd probably consider CSS Element Queries and CQ Prolyfill if I had to choose. Of these CSS Element Queries doesn't extend existing CSS at all and you don't need a post-processor, while CQ uses :container
selector that feels very native CSS-like.
In comparison EQCSS seems like a syntax that won't get implemented, and Container Query seems like a lot of work to get into actual use - which might be partially due to how it's documentation is currently structured, giving a complete but heavy feel.
Ones to avoid
These have a little popularity, but the other options are simply better.
-
react-element-query
: 8.7 kB and is now badly outdated, having had no updates in over two years, and is based on window resize event. The syntax is also opined towards breakpoints instead of queries so you get lot of code for a very few features. -
remeasure
: at 7.3 kB I'd pickreact-measure
over this one if I needed to have other measurements than width and height. -
react-bounds
: 7.9 kB and no updates in three years. Useselement-resize-detector
likereact-sizeme
does. -
react-component-query
: 5.0 kB and depends onreact-measure
, you end up with less code implementing your own based onreact-measure
. -
react-container-query
: 6.5 kB only to get strings for className. -
react-queryable-container
: 1.9 kB but uses window resize event, thus avoid.
Further reading
-
Daniel Buchner:
Cross-Browser, Event-based, Element Resize Detection
From 2013, the
<object data="about:blank" type="text/html" />
trick. -
Tyson Matanich:
Media queries are not the answer: element query polyfill
From 2013: points out the circularity issues with practical examples.
-
Mat Marquis:
Container Queries: Once More Unto the Breach
From 2015, uses
:media
pseudo selector. -
Matthew Dean:
From January 2019, the latest attempt but does not solve the issues; you can also find other proposals via this link!
-
Chris Coyier:
Let's Not Forget About Container Queries
From September 2019, gives another kind of viewpoint into the subject.
Want to help?
In summary a typical issue with proposals and specs so far has been that they attempt to tackle too many things, having too many features without solving the core issue of circularity that would make implementing a standard into CSS a reality. I'd argue having more of this is something we don't need. Rather, solving the main issue requires someone to be able to dig in to the inner workings of CSS and browsers.
If you want to have a go at this check out WICG's Use Cases and Requirements for “Container Queries” as going through those can help greatly in shaping what really needs to be accomplished.
My tip for the interested: forget about all current syntaxes and media queries, instead try to find what is common and what is the need, as the real solution for those might be very different from measurement of sizes. Why? Because so far as a community all we've done is to bang our heads to the wall of circularity.
I hope circularity and browser render logic issues can eventually be figured out so that we get Container Queries, or a good alternative native CSS standard!
Top comments (0)