You add one line of CSS to a search form. Then the form is gone.
Well, not gone. DevTools says it is 2px wide. That is just its left and right border.
The line is container-type: inline-size. The plan was simple: the form should switch from a stacked layout to a side-by-side layout when it has room. Instead, it has no room at all.
We rebuilt this bug on a test page to measure it. In our tests it came down to one rule, so let us look at that rule.
The setup
Here is a cut-down version of the page. A header bar holds a logo on the left and a search form on the right.
<header class="toolbar">
<div class="logo">Logo</div>
<form class="search">
<div class="search-inner">
<input type="search" placeholder="Search">
<button>Go</button>
</div>
</form>
</header>
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.search {
container-type: inline-size; /* new: this is what breaks it */
border: 1px solid #333;
}
.search-inner {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
@container (min-width: 30rem) {
.search-inner {
flex-direction: row;
}
}
Before the container-type line, the form was as wide as its input and button. After it, the form measured 2px in Chrome 156 (we checked with getBoundingClientRect()). The input and button spilled out over the edge.
What went wrong
A query container has a deal with the browser. The browser agrees to work out the container's width without looking at its children.
The browser needs that deal. Your @container rules change the children. If the children could also change the container's width, the two would keep changing each other forever.
MDN puts it like this for inline-size:
"The inline size of the element can be computed in isolation, ignoring the child elements"
Now look at the toolbar again. The search form is a flex item. It has no width and no flex-grow. So how wide is it? As wide as its content.
But the browser just promised to ignore the content. So the content counts as zero. All that is left is the border: 1px on each side, 2px total.
MDN says the same thing in plain words:
"The container size has to be set by context, such as block-level elements that stretch to the full width of their parent, or explicitly defined. If a contextual or explicit size is not available, elements with size containment will collapse."
So the rule to remember is short. A container must get its width from outside, never from inside.
Places where width comes from inside
These are the spots where an element sizes itself to its content. A container in any of them will collapse.
- A flex item in a row with no
width,flex-basisorflex-grow. (Our bug.) - A child of a column flexbox that uses
align-items: center. - An element with
width: fit-contentorwidth: max-content. - Floats,
inline-blockelements and absolutely positioned boxes with no width. - A grid item that is centered with
justify-self: center.
We tried the first three in Chrome 156. All three came out 2px wide, which was just their borders. The same form inside a column flexbox with the default align-items: stretch was full width, as expected.
Fix 1: Let the flex item grow
In a flex row, give the container a share of the free space.
.search {
container-type: inline-size;
flex: 1 1 20rem;
}
Now the width comes from the toolbar, which is outside. In our test the form filled the rest of the 1000px toolbar, and the @container rule switched the layout to a row. When we made the page 400px wide, it switched back to a column. That is the behavior the form was meant to have from the start.
Fix 2: Give it a real width
If you don't want it to grow, set a width that does not depend on the children.
.search {
width: min(100%, 32rem);
container-type: inline-size;
}
min() picks the smaller value. On a wide screen the form is 32rem. On a small screen it can't get wider than its parent. Either way, the number comes from outside the form.
Fix 3: Move the container up one level
Sometimes you can't touch the width of the element. Maybe it comes from a shared component.
In that case, make a different element the container. Pick an ancestor that already stretches, like a plain block div. Then query that one.
<div class="search-area">
<form class="search">...</form>
</div>
.search-area {
container: search-area / inline-size;
}
@container search-area (min-width: 30rem) {
.search-inner { flex-direction: row; }
}
The container shorthand sets a name and a type at once. The name lets your @container rule target this exact box, even if other containers sit around it.
One thing to keep in mind: an @container rule styles the container's descendants, not the container itself. So put the container one level above the element you want to change.
A trap with container-type: size
size is stricter than inline-size. It ignores the children for the width and the height.
.preview {
container-type: size;
width: 400px;
}
This box has a width, but no height. In our test it came out 400px wide and 0px tall, so its text spilled out.
If you need size, give the box a height that does not come from its content. A fixed height works. So does aspect-ratio:
.preview {
container-type: size;
width: 400px;
aspect-ratio: 16 / 9;
}
That gave us a 400 by 225 box. For most layouts, though, inline-size is all you need.
Find collapsed containers in one go
Paste this in the DevTools console. It lists every container on the page that is less than 10px wide.
for (const el of document.querySelectorAll('*')) {
const type = getComputedStyle(el).containerType;
if (type !== 'normal' && el.getBoundingClientRect().width < 10) {
console.warn('Collapsed container?', el, type);
}
}
normal is the default value of container-type, so anything else is a container. On our test page this found all three broken forms and none of the fixed ones.
In Chrome DevTools, the Elements panel also puts a container badge next to query containers. That makes them easy to spot.
Browser support
MDN marks container-type as Baseline widely available and says "It's been available across browsers since February 2023." Our own tests ran in Chrome 156 only. We did not test Firefox or Safari this time, so please check the layout there too.
The short version
Adding container-type tells the browser to size the box without its children. If the box was relying on its children for its width, it shrinks to its borders. Give the width a source from outside: flex-grow, an explicit width, or a stretching parent that acts as the container instead.
Sources
- MDN,
container-type: https://developer.mozilla.org/en-US/docs/Web/CSS/container-type - MDN, CSS container queries: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
- MDN, Using CSS containment: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Using_CSS_containment
- Chrome DevTools, Inspect CSS container queries: https://developer.chrome.com/docs/devtools/css/container-queries
- CSS Conditional Rules Module Level 5, container queries (W3C): https://www.w3.org/TR/css-conditional-5/
At PIXEL RECRUITING LLC we place frontend engineers who really understand CSS layout, including container queries and responsive components. We are also a direct staffing partner for Mastercard in O'Fallon, MO. Hiring, or looking for your next UI role? Start at pixelrecruiting.com.
Top comments (0)