Container Queries: Another Polyfill
Tags: javascript, css, showdev, webdev
Published: 2021-05-26 11:50:27

I **love** container queries — I have been waiting for them for years.

But, alas, until all browsers have implemented them, we have to rely on polyfills to make them work. 

While other polyfills work just fine, I needed something that didn't require postCSS or a specific syntax – and more tailored to a project, I'm currently working on. 

So I decided to make my own polyfill, and ended up with a script, that's just 502 bytes gzipped:

```js
if(!("CSSContainerRule"in window)){const e=(e,s)=>e.reduce((e,t,c)=>s(t)?c:e,-1),s=new ResizeObserver(s=>{for(let t of s){const s=t.target,c=s.__cq,n=e(c.bp,e=>e<=t.contentRect.width);n!==s.index?(s.style.cssText=c.css.filter((e,s)=>s<=n).join(""),c.index=n):-1===n&&s.removeAttribute("style")}});[...document.styleSheets].map(e=>{fetch(e.href).then(e=>e.text()).then(e=>{let t,c=new Set;const n=/@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm;for(;t=n.exec(e);)[...document.querySelectorAll(t.groups.selector)].forEach(e=>{e.__cq=e.__cq||{bp:[],css:[],index:-1};const s=t.groups.breakpoint-0,n=t.groups.css,o=e.__cq.bp.findIndex(e=>e===s);o<0?(e.__cq.bp.push(s),e.__cq.css.push(n)):e.__cq.css[o]=e.__cq.css[o].concat(n),c.add(e)});for(let e of c)s.observe(e)})})}
```

OK, that's *completely* unreadable, so let's set up the stage with HTML and CSS, before we look at the script!

---

## Setting the stage

In HTML, add this to a new document:

```html
<main>
  <div class="cqw"><div class="cq cq1"></div></div>
  <div class="cqw"><div class="cq cq2"></div></div>
  <div class="cqw"><div class="cq cq3"></div></div>
  <div class="cqw"><div class="cq cq4"></div></div>
</main>
```

In the `<head>`-section, add a link to a stylesheet:

```html
<link href="cq.css" rel="stylesheet">
```

Now, create the `cq.css`-sheet:

```css
body {
  margin: unset;
}
main { 
  display: flex;
  flex-wrap: wrap;
}
.cq {
  aspect-ratio: var(--asr, 1);
  background-color: var(--bgc, silver);
  width: var(--w, 25vw);
}
.cqw {
  contain: layout inline-size;
}
.cq1 { --bgc: tomato }
.cq2 { --bgc: orange }
.cq3 { --bgc: skyblue }
.cq4 { --bgc: tan; }

@container (min-width: 300px) { .cq { --asr: 2/1; } }
@container (min-width: 300px) { .cq1 { --bgc: indianred; } }
@container (min-width: 300px) { .cq2 { --bgc: darkorange; } }
@container (min-width: 300px) { .cq3 { --bgc: steelblue; } }
@container (min-width: 300px) { .cq4 { --bgc: lavender; } }
@media (min-width: 600px) { .cq { --w: 50vw; } }
@media (min-width: 900px) { .cq { --w: 25vw } }`
```

Your page should now look like this:

![CQ example 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vlyu9507myq7lu4zo5gx.png)

---

## The Script
First we need to check whether we need the script or not: 

```js
if (!('CSSContainerRule' in window))
```

Next, we'll iterate the stylesheets on the page, grab them (again, but they are cached) with `fetch()`, convert the result with `.text()` and return the rules as a string:

```js
[...document.styleSheets].map(sheet => {
  fetch(sheet.href)
    .then(css => css.text())
    .then(rules => { ... }
```

We'll use `regEx` to find what we need in that string: 

```js
const re = /@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm
```

> **NOTE:** A good place to play around with RegEx, is [regex101.com](https://regex101.com/)

This expression will return groups of matches entitled `breakpoint`, `selector` and `css`.

Now, let's iterate the matches. For each match, we'll use a `querySelectorAll` to find the elements in the `DOM` matching the `selector`.

On each element, we'll create an object, `__cq` that will contain an array of breakpoints, the css for each breakpoint, and an index. For each iteration, we'll check whether the object already exists:

```js
let match;
let observe = new Set();
while (match = re.exec(rules)) {
  [...document.querySelectorAll(match.groups.selector)].forEach(elm => {
    elm.__cq = elm.__cq || { bp: [], css: [], index: -1 }
    const bp = match.groups.breakpoint-0;
    const css = match.groups.css;
    const index = elm.__cq.bp.findIndex(item => item === bp);
    if (index < 0) {
      elm.__cq.bp.push(bp);
      elm.__cq.css.push(css);
    }
    else {
      elm.__cq.css[index] = elm.__cq.css[index].concat(css);
    }
    observe.add(elm);
  })
}
```

A `Set()` called `observe` is used to hold the (unique) set of elements, we'll need to observe:

```js
for (let item of observe) RO.observe(item);
```

`RO` is a `ResizeObserver`:

```js
const RO = new ResizeObserver(entries => {
  for (let entry of entries) {
    const elm = entry.target;
    const cq = elm.__cq;
    const lastIndex = findLastIndex(cq.bp, item => item <= entry.contentRect.width);
    if (lastIndex !== elm.index) {
      elm.style.cssText = cq.css.filter((item, index) => index <= lastIndex).join('');
      cq.index = lastIndex;
    }
    else if (lastIndex === -1) elm.removeAttribute('style');
  }
});

```

It's using a small method called `findLastIndex`:

```js
const findLastIndex = (items, callback) => items.reduce((acc, curr, index) => callback(curr) ? index : acc, -1);
```

... and use that to determine which `breakpoint` (bp) is currently needed, and then sets the `style>`-attribute of the element to the `css` from the `__cq`-object.

Here's the complete script — add this or the minified version above to a `<script>`-tag on your demo-page:

```js
if (!('CSSContainerRule' in window)) {
  const findLastIndex = (items, callback) => items.reduce((acc, curr, index) => callback(curr) ? index : acc, -1);
  const RO = new ResizeObserver(entries => {
    for (let entry of entries) {
      const elm = entry.target;
      const cq = elm.__cq;
      const lastIndex = findLastIndex(cq.bp, item => item <= entry.contentRect.width);
      if (lastIndex !== elm.index) {
        elm.style.cssText = cq.css.filter((item, index) => index <= lastIndex).join('');
        cq.index = lastIndex;
      }
      else if (lastIndex === -1) elm.removeAttribute('style');
    }
  });

  [...document.styleSheets].map(sheet => {
    fetch(sheet.href)
      .then(css => css.text())
      .then(rules => {
        let match;
        let observe = new Set();
        const re = /@container\s?\(min-width:\s?(?<breakpoint>.*)px\)\s?\{\s?(?<selector>.*)\s?\{\s?(?<css>.*;)\s?\}/gm
        while (match = re.exec(rules)) {
          [...document.querySelectorAll(match.groups.selector)].forEach(elm => {
            elm.__cq = elm.__cq || { bp: [], css: [], index: -1 }
            const bp = match.groups.breakpoint-0;
            const css = match.groups.css;
            const index = elm.__cq.bp.findIndex(item => item === bp);
            if (index < 0) {
              elm.__cq.bp.push(bp);
              elm.__cq.css.push(css);
            }
            else {
              elm.__cq.css[index] = elm.__cq.css[index].concat(css);
            }
            observe.add(elm);
          })
        }
        for (let item of observe) RO.observe(item);
      }
    )
  })
}

```

Now, when you resize your page, the boxes change `aspect-ratio` and `background-color`:

![CQ example 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udnrqk46x8ivrq3yd8x0.png)

At `900px` the layout returns to it's initial values, and then at `1200px` it's back to the updated values.

> **NOTE:** It's *mobile first*, thus only `min-width` will work, and **only** with pixels as it's value, since the `contentRect.width` of the `ResizeObserver` returns a value in pixels.

I'm sure there's a ton of stuff that could be optimized or changed/added (error-handling, for instance!) — after all, this is something I cooked up in 3-4 hours!

The Codepen below works best, if you open/edit it on Codepen, and resize the browser:

{% codepen https://codepen.io/stoumann/pen/GRWMZOW %}

Thanks for reading! 

---
Cover-image by Pixabay from Pexels