Karl Oskar Anderson

Home / Blog / Container queries suck

Container queries suck

By Karl Oskar Anderson on 03.03.2024

I was working on an application that needed to responsively fit into an unknown viewport on external site. This seemed like a ideal problem to solve using container queries. Unfortunately, container queries did not solve this issue without side effects. Let's look deeper into the problem and find a fitting solution.

These issues can be seen in the attached Codepen example.

See the Pen Container query by Karl Oskar Anderson (@Karl-Oskar-Anderson) on CodePen.

Introduction

Traditionally, CSS media queries would be used to make responsive components that look good on both desktop and mobile. Media queries work based on the dimensions of the site viewport. This works alright, but placing components inside a container smaller than the viewport like a sidebar can cause problems especially with flex-direction and grid-column rules.

Chrome 106 released in September 2022 offered a solution to this problem - container queries. Container query allows styling components based on the parent element size. Container queries work by defining a containment context that works as a viewport for all of its children's elements. This is done by giving the parent element container-type: inline-size; CSS rule. At this point it might seem like container queries are the new media queries, however they have drawbacks.

Problems with modals

The first problem is that fixed/absolute elements inside the container cannot expand past the container. The container creates a containing block that behaves as a relative element. This makes it impossible to have dropdown inputs open as full screen modals on smaller screen sizes.

This problem can be remedied by disabling the container query when a full screen modal is supposed to be visible. However, this solution is not ideal as it is impossible to animate the transition to the full screen modal view.

.inline-size {
    container-type: inline-size;
}
.inline-size:has([data-modal-open="true"]) {
    container-type: normal;
}

There is also a second problem caused by hiding the window scrollbar when the modal is open. This causes the container to recalculate its size indefinably as the scrollbar will appear and disappear.

Stacking context issue

Another problem is that container queries mess with element stacking order causing dropdown elements that expand past the container to be clipped by relative elements outside the dropdown. This can be somewhat fixed by specifing the stacking order of the outside elements. This only works if the other relative elements have a lower z-index value, so that would not be a very scaleable solution.

.inline-size {
    container-type: inline-size;
    position: relative;
    z-index: 1;
}

Problem overview

Container rule along with rules such as filter, transform and backdrop-filter create a containing block that acts as being positioned relatively and having a z-index of 0. This results in components like modal based inputs and dropdowns not to work inside a container query. The containing block traps the children's elements from escaping out of the container query. Let's see how container size based responsiveness could be achieved by using JavaScript instead of container queries.

Solution

Instead of using classes directly, lets write the responsive style rules inside HTML data-resize-responsive attributes. The format for this would be JSON that can be converted to type { target: string, breakpoints: { [id: number]: string }}. This format allows specifing the target container whose dimensions will determine the correct breakpoint rule to be used.

Next a window resize event handler needs to be added to detect changes to the window size that will in turn cause changes to every other element. When a resize event fires all elements with a data-resize-responsive attribute will be selected, parsed and applied like this:

function addResizeListeners(detachedEventSignal) {
    const resizeHandler = () => {
        const elements = Array.from(document.querySelectorAll('[data-resize-responsive]'));
        for (const element of elements) {
            const responsiveness = JSON.parse(element.dataset.resizeResponsive);
            const container = document.querySelector(responsiveness.target);
            const breakpointEntries = Object.entries(responsiveness.breakpoints)
                .map(x => ({ key: Number(x[0]), value: x[1] }))
                .sort((a, b) => Number(a.key) - Number(b.key));
            
            let newRules = breakpointEntries.findLast(x => container.clientWidth >= x.key)?.value ?? "";
            
            const possiblyAddedClasses = Array.from(new Set(breakpointEntries.flatMap(x => x.value.split(" "))));
            const classesToRemove = Array.from(element.classList).filter(x => 
                possiblyAddedClasses.includes(x) && 
                !newRules.split(" ").includes(x)
                                                                                    )
            const classesToAdd = newRules.split(" ").filter(x => !element.classList.contains(x) && x !== "");
            
            element.classList.remove(...classesToRemove);
            element.classList.add(...classesToAdd);
        }
    }
 
    window.addEventListener('resize', resizeHandler, { signal: detachedEventSignal });
    resizeHandler();
}

This solution manages to achieve the same desired responsive design without bumping into problems with container queries. The solution works best when paired with a CSS utility framework like Tailwind to prevent manual work writing utility classes.

Wrap up

I was looking forward to expanding my design skill repertoire with container queries, but unfortunately found them problematic. This article demonstrated problems that arise with container queries and offered an alternative solution to solving them.