Container Queries And Element Resize Detection As We Enter 2020
Tags: react, css, javascript, codequality
Published: 2019-12-17 10:15:34

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](https://caniuse.com/#search=container%20query) despite having numerous JS libraries tackling the issue and even large and detailed proposals.

```css
@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**!

```scss
/* 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](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) which has support in [Chrome, Firefox and Samsung Internet](https://caniuse.com/#feat=mdn-api_resizeobserver) and there is a [polyfill available](https://www.npmjs.com/package/@juggle/resize-observer) 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!

```jsx
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`!

```jsx
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:

1. Parent container **must** have `position` other than `static`.
2. `<object />` element **must** be hidden visually AND interactively.
3. `<object />` **will** mess up with some CSS by existing within the container, most likely `:first-child` or `:last-child`.
4. Container **should not** have border or padding.

Taking all of the above into account the final CSS and HTML needed would look like this:

```css
/* 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%;
}
```

```html
<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:

```js
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](https://www.npmjs.com/package/react-sizeme) **<small><small>([8.2 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-sizeme))</small></small>**

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](https://www.npmjs.com/package/react-measure) **<small><small>([3.9 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-measure))</small></small>**

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

- [react-element-size](https://www.npmjs.com/package/react-element-size) **<small>([2.9 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-element-size))</small>**
- [react-use-size](https://www.npmjs.com/package/react-element-size) **<small>([3.1 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-use-size))</small>**

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](https://www.npmjs.com/package/react-resize-aware) **<small><small>([0.61 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-resize-aware))</small></small>**

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](https://www.npmjs.com/package/styled-container-query) **<small><small>([5.6 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/styled-container-query))</small></small>**

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](https://github.com/FreddyFY/styled-container-query/issues/6) 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](https://github.com/Merri/react-use-queries) **<small><small>([0.75 kB minzipped](https://flat.badgen.net/bundlephobia/minzip/react-use-queries))</small></small>**

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**:

    [CSS Element Queries](http://marcj.github.io/css-element-queries/)

    First released in 2014, widely browser compatible utility that is based on `min-width` and `max-width` attribute selectors.
- **Martin Auswöger**:

    [CQ Prolyfill (container queries speculative polyfill)](https://github.com/ausi/cq-prolyfill)

    First released in 2015, uses `:container` pseudo selector.
- **Tommy Hodgins**:

    [EQCSS: Element Query CSS](https://elementqueries.com/)

    First released in 2017, has [a very complete spec](https://github.com/tomhodgins/element-queries-spec), but has verbose `@element` syntax.

- **Viktor Hubert**:

    [Container Query](https://github.com/ZeeCoder/container-query)

    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`](https://www.npmjs.com/package/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`](https://www.npmjs.com/package/remeasure): at 7.3 kB I'd pick `react-measure` over this one if I needed to have other measurements than width and height.
- [`react-bounds`](https://www.npmjs.com/package/react-bounds): 7.9 kB and no updates in three years. Uses `element-resize-detector` like `react-sizeme` does.
- [`react-component-query`](https://www.npmjs.com/package/react-component-query): 5.0 kB **and** depends on `react-measure`, you end up with less code implementing your own based on `react-measure`.
- [`react-container-query`](https://www.npmjs.com/package/react-container-query): 6.5 kB only to get strings for className.
- [`react-queryable-container`](https://www.npmjs.com/package/react-queryable-container): 1.9 kB but uses window resize event, thus avoid.

## Further reading

- **Daniel Buchner**:

    [Cross-Browser, Event-based, Element Resize Detection](http://www.backalleycoder.com/2013/03/18/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](https://www.smashingmagazine.com/2013/06/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](https://alistapart.com/article/container-queries-once-more-unto-the-breach/)

    From 2015, uses `:media` pseudo selector.
- **Matthew Dean**:

    [Container Queries proposal](https://github.com/WICG/container-queries/issues/12)

    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](https://css-tricks.com/lets-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”](https://github.com/WICG/cq-usecases) 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!