size container Missing: cqh Falls Back to Viewport, Not Off
This article may contain affiliate links. Its content is not affected by advertising.
In short
cqh doesn't go inert without a container-type: size ancestor — it resolves against the next size container up the tree, or against the viewport if there is none.
The short version
Using cqh in a path with no container-type: size ancestor doesn’t error out. It silently resolves against the next size container found further up the tree — or, if there is none at all, against the small viewport height. Assuming “container-type: normal disables cqh” missed an existing mobile branch (which deliberately set container-type: normal to switch to scrollable layout) and reproduced the same font-size runaway it was supposed to prevent.
The symptom
Kimiterrace’s digital signage board (built for wall-mounted TVs) sizes each schedule row’s font with 100cqh / var(--visible-rows), scaling text to the row’s height. This worked fine on desktop and on the actual TV hardware, where the font-to-row-height ratio (a 0.48 coefficient, about 60% of row height) had already been verified on real devices.
On 2026-09-10, opening the public /signage page on a phone (≤899px) showed subject names rendered as a single illegible blob, with the notices and submissions sections pushed off-screen. The font size had inflated all the way to the clamp() ceiling of 3rem (48px). The admin editor’s live preview did not reproduce this at all.
Why
The mobile breakpoint (≤899px) has a pre-existing @media branch that switches the layout to a vertically-scrolling stack. As part of that, it deliberately reverts .autoScroll — the schedule rows’ size container — back to container-type: normal. That’s an intentional change, predating this cqh migration, meant to drop the fixed row height and let content flow at its natural height instead:
/* signage.module.css: 2376-2382 (before the fix; unchanged by it) */
/* Mobile scrolls the whole page, so the fixed-viewport window and fixed row height (§11a)
are disabled here; content flows at natural height for all items (consistent with the
card's overflow:visible). The fixed layout is TV-wall-only. */
.autoScroll {
container-type: normal;
overflow: visible;
}
.autoScrollItem {
height: auto;
}
The very comment attached to that mobile branch already contained the assumption that caused this bug:
/* signage.module.css: 289-312 (before the fix)
.scheduleListItem {
/* cqh fallback: WebViews without container query support get the first
declaration (vw-based); supporting browsers get the second (scroll-area
height based). Intentional duplicate declaration. */
font-size: clamp(0.78rem, 1.3vw, 1.2rem);
/* …
⚠ On mobile (≤899px), .autoScroll becomes container-type: normal,
which disables cqh — falling through to the vw version above. */
/* biome-ignore lint/suspicious/noDuplicateProperties: cqh fallback (unsupported browsers get the vw version above) */
font-size: clamp(0.7rem, calc(100cqh / var(--visible-rows, 7) * 0.48), 3rem);
…
}
That duplicate declaration was originally progressive enhancement aimed at a different axis entirely: “browsers without container query support get line 1 (the vw version), browsers with support get line 2 (the cqh version).” The mobile container-type: normal change is an unrelated axis — the browser still fully supports container queries, it’s just that .autoScroll itself stopped being a size container.
What actually happens here is not “cqh gets disabled and falls back to the vw line.” cqh, lacking a size container directly among its ancestors, keeps looking further up the tree for the next size container — and if none exists at all, resolves against the small viewport height as a valid value. In this case there was no other size container above .autoScroll, so 100cqh resolved against the viewport height (844px). Since the second declaration (the cqh version), written later in the source, still produces a valid value, it’s the one that wins — the cascade never falls through to line 1 (the vw version) at all.
On top of that, .autoScroll .scheduleListItem (specificity 0,2,0) outranks the separate @media (max-width: 899px) rule targeting .scheduleListItem alone (specificity 0,1,0) that was defined elsewhere for the same mobile width. Stacking another font-size override onto that rule couldn’t have beaten the ancestor-qualified cqh declaration anyway.
The fix
Rather than piling another override onto a specific class or media query, the font-size declaration itself was restructured so it only parses in the first place along paths that actually have a size container ancestor:
/* signage.module.css: 907-939 (after the fix) */
.autoScroll {
flex: 1;
min-height: 0;
overflow: hidden;
container-type: size;
display: flex;
flex-direction: column;
}
.autoScrollItem {
height: calc(100cqh / var(--visible-rows));
box-sizing: border-box;
}
@container (min-height: 0px) {
.autoScroll .scheduleListItem {
font-size: clamp(0.7rem, calc(100cqh / var(--visible-rows, 7) * 0.48), 3rem);
}
}
An @container query simply doesn’t match — and every declaration inside it is skipped entirely — for any element with no query container among its ancestors at all. On the mobile branch, where .autoScroll is container-type: normal, there is no query container in scope, so this whole @container block is excluded from matching and the cqh declaration for .autoScroll .scheduleListItem simply never applies. What’s left on mobile is just the first, unscoped declaration: font-size: clamp(0.78rem, 1.3vw, 1.2rem) (the vw version).
On desktop and the real TV hardware (where .autoScroll stays container-type: size), a query container is present, so the @container block matches normally and font size continues scaling proportionally to row height exactly as before.
/* signage.module.css: 2611-2615 (after the fix; admin editor preview only) */
.scaledStage .autoScroll {
container-type: size;
}
.scaledStage .autoScrollItem {
…
}
The admin editor’s live preview (.scaledStage) is a separate code path that already restores container-type: size on .autoScroll even below 899px, because the preview is deliberately rendered as a fixed 16:9 stage. That’s exactly why this bug never reproduced in the preview — it never takes the mobile branch at all. It only surfaced the first time someone opened the public URL on an actual phone.
Preventing a repeat
container-type: normal means “this element is no longer a size container” — it is not a switch that disables cqh itself. Container query units (cqh, cqw, cqi, etc.) keep searching further up the ancestor chain for the nearest size container when the current path doesn’t have one, ultimately falling back to the small viewport. “No container means disabled” is an intuitive assumption, but it doesn’t match the actual spec — and having written that assumption down in a comment once didn’t stop it from being missed in the moment.
When the same class is reused across multiple container-type contexts (desktop = size, mobile = normal, admin preview = size restored via a separate path), each path needs to be checked individually for whether a declaration containing a container query unit leaks into a path with no size container at all. Rather than stacking more overrides tied to specific class names or media queries, scoping the declaration itself to “only paths with a size container” via an @container query holds up better as similar paths get added later.
Frequently asked questions
Q1What happens if cqh is used with no size container in scope?
It doesn't error and it doesn't go inert. 100cqh resolves against the next size container found further up the ancestor chain, or against the small viewport height if none exists at all. There's no 'disabled because no container' behavior.
Q2Does container-type: normal disable cqh?
No. It only stops that element from being a size container itself — cqh units inside it keep looking further up the ancestor chain, or fall back to the viewport. Here that mismatch let font size run to the clamp() ceiling of 3rem on a mobile branch.
Q3Why didn't the existing vw fallback declaration kick in?
It was progressive enhancement for browsers without container query support, not for a supporting browser where cqh resolves against the viewport instead of a container. Written second, the cqh line always wins in a supporting browser regardless of whether a size container exists.
Q4Why does @container (min-height: 0px) fix it?
An @container query simply doesn't match — and every declaration inside it is skipped — on any element with no query container among its ancestors. That lets you scope the font-size rule to only the paths that actually have a size container, in pure CSS.
Environment verified
- Next.js ^16.0.0 / React ^19.0.0 (キミテラス-v2 signage.module.css)
- 2026-09-10, found and fixed roughly 52 minutes apart. Found by opening the public /signage page on a real phone; did not reproduce in the admin editor preview.
What this article is based on
- CSS file lines 289-312commit c4f0a31
- CSS file lines 2376-2382commit c4f0a31
- CSS file lines 907-939commit eda0843
- CSS file lines 2611-2615commit eda0843
Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.