Skip to content
R.

Why do container queries stop matching when you switch container-type dynamically?

A container query can look correct in a static demo and still fail after a runtime class flip if the container no longer supports the axis you are querying. Use the right `container-type` for the query axis, or keep the named container context available when you need name-only matching.

About 12 min readComments

Container queries can seem to fail for no obvious reason when a component switches classes at runtime. The bug often is not the breakpoint itself. It is the container context that the query depends on.

This article uses a small deterministic fixture to show why a width-based query can survive a toggle while a height-based query stops matching, and why naming a container helps only when the browser still has the right axis available.

The repair is to match the container type to the axis you query, or to keep the named container context attached to the element that must remain queryable.

Why a container query can stop matching after a class change

A container query is not just a breakpoint with a different selector. It depends on the container still being eligible for the kind of query you wrote. That distinction is easy to miss when a component starts with one class and then flips to another at runtime. The query can look stable in the static state and then suddenly stop matching after a toggle because the container context changed, not because the content became less responsive.

The useful mental model is that the browser first asks whether there is an eligible container ancestor, then checks the relevant axis or name. If the container loses the required containment mode, the query does not simply become smaller or larger. It may stop having a valid target at all. That is why a dynamic state change can create a mismatch that looks like a bug in the query, even though the real problem is the container context you are querying against.

The supplied fixture models that failure with a small tree: a target element inside a container whose `container-type` changes from `size` to `inline-size`. In the working state, both inline-axis and block-axis queries have a matching ancestor. After the toggle, the inline-axis query still has a valid container, but the block-axis query no longer does. That is the key diagnostic: if only one axis survives the toggle, the container type—not the `@container` syntax—is usually the thing to inspect first.

main.mjs / javascript
export function createElement({ tag = 'div', width = 0, height = 0 } = {}) {
  return {
    tag,
    width,
    height,
    parent: null,
    children: [],
    containerType: 'normal',
    containerName: undefined,
  };
}

export function appendChild(parent, child) {
  child.parent = parent;
  parent.children.push(child);
}

export function setContainerType(el, type) {
  el.containerType = type;
}

export function setContainerName(el, name) {
  el.containerName = name;
}

export function toggleContainerType(el, nextType) {
  setContainerType(el, nextType);
}

function compare(op, left, right) {
  switch (op) {
    case '>': return left > right;
    case '>=': return left >= right;
    case '<': return left < right;
    case '<=': return left <= right;
    default: throw new Error(`unsupported compare operator: ${op}`);
  }
}

function queryAxis(query) {
  if (query.kind === 'name-only') return null;
  if (query.axis) return query.axis;
  if (query.feature === 'width') return 'inline';
  if (query.feature === 'height') return 'block';
  return null;
}

export function findEligibleContainer(startEl, query) {
  let node = startEl.parent;
  const axis = queryAxis(query);

  while (node) {
    if (query.kind === 'name-only') {
      if (node.containerName === query.name) return node;
    } else if (axis) {
      if (node.containerType === 'size') return node;
      if (node.containerType === 'inline-size' && axis === 'inline') return node;
    }
    node = node.parent;
  }

  return null;
}

export function evaluateQuery(targetEl, query) {
  const container = findEligibleContainer(targetEl, query);
  if (!container) {
    return { matched: false, reason: 'no-eligible-container', container: null };
  }

  if (query.kind === 'name-only') {
    return { matched: true, container };
  }

  const axis = queryAxis(query);
  const size = axis === 'inline' ? container.width : container.height;
  return {
    matched: compare(query.compare, size, query.value),
    container,
    containerValue: size,
  };
}

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

Match the container type to the axis you are querying

`container-type: size` and `container-type: inline-size` do not mean the same thing. MDN describes `size` as enabling queries on both inline and block dimensions, while `inline-size` only enables the inline dimension. That matters because a query that inspects height, block size, or another vertical measure needs a container that still supports block-axis matching after the runtime change.

This is the first repair pattern: choose the container type that matches the axis you actually query. If the layout only ever needs width-based behavior, `inline-size` is usually enough and avoids the extra containment cost of `size`. If the component contains a block-axis query, switching to `inline-size` later will make that query stop matching by design. The browser is not ignoring the rule; the container no longer qualifies for that axis.

The fixture makes this visible by asking one query against width and another against height. When the container is `size`, both queries match. When the container flips to `inline-size`, the width-based query still matches, but the height-based query fails because there is no longer an eligible container for that axis. That difference is exactly what a runtime toggle can hide if you only inspect the final DOM without checking which axis the query depends on.

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

A name can preserve targeting, but it does not change axis eligibility

Naming a container solves a different problem from sizing it. `container-name` lets you point a query at a specific container instead of whichever eligible ancestor happens to be nearest. That is useful when a component can sit inside several nested containers and you want to avoid accidental matching against the wrong one. It is not, by itself, a substitute for the right `container-type`.

The second repair pattern is therefore not “add a name and everything works.” A named container can still fail an axis query if its `container-type` does not support that axis. Name-only queries behave differently because they are about identifying the container context, not measuring a size condition. The browser still needs the container to exist, but the query’s success criterion changes from “is this the named container?” to “is this the named container?” for name-only queries, while axis-based queries still require the axis to be supported.

That distinction explains a common debugging dead end. Developers sometimes add `container-name` during a refactor, then flip `container-type` later and assume the name should keep the query alive. The name can help re-establish which container the browser targets, but it cannot manufacture support for a block-axis size query on a container that only advertises inline-size containment. If the query is axis-based, the axis wins.

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

Read the deterministic fixture in the same order the browser does

The supplied example is intentionally small because the failure is about eligibility, not about a large application. The model creates a root container, a target element, and a runtime toggle that changes the root from `size` to `inline-size`. It also includes a name-only case so the article can separate name targeting from axis-based matching. That split is the important part of the diagnostic.

The first test establishes the baseline: with `container-type: size`, both the inline and block queries match. The second test applies the runtime toggle and checks that the inline query still matches while the block query no longer does. The third test adds a named container to show that naming alone does not fix an axis query, and that restoring `container-type: size` repairs the block-axis match. That sequence is more useful than a single happy-path screenshot because it shows the intermediate failure state, not just the final answer.

The value of this kind of fixture is that it tells you which assumption broke. If the width-based query survives but the height-based query disappears, the content did not become too small. The browser simply no longer has a qualifying container for that axis. If a name-only query still resolves, then the named context is still present and the bug lies in the axis requirement, not in container discovery.

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

Choose the repair that matches the failure you observed

If the problem is that a block-axis query stops matching after a runtime change, the direct repair is to keep a `size` container in place wherever that query must continue to work. That can mean moving the query to a different wrapper, leaving the queried container on `size`, or separating the layout state from the container context so the runtime change does not remove the required axis support.

If the problem is that the query targets the wrong container after a refactor, reintroduce or restore the `container-name` that the rule expects. That fixes the targeting problem, not the axis problem. The article’s fixture uses this to show an important boundary: a name can make the browser look at the intended ancestor, but it cannot make an `inline-size` container satisfy a height-based query. The fix has to match the cause.

The incomplete fix is to assume that switching classes only changes the visual mode. In a container-query setup, changing a class can also change the container contract. That is why the same toggle can preserve one query and break another. A reliable repair therefore checks two things separately: which ancestor is eligible, and which axes that ancestor supports.

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

What this diagnostic does not prove

This article proves the containment logic you need to inspect, not every browser quirk around container queries. The fixture is deterministic because it models the documented eligibility rules, but your real app still needs browser-level verification for the actual layout, the actual selectors, and any other stylesheet rules that interact with the query. A local diagnosis can show that the container axis changed; it cannot automatically prove that every browser has the same rendering result in every nested layout.

There are also two important applicability limits. First, if a query fails because the container is too narrow or because another selector overrides the result, the fix is not to change `container-type`. Second, if the problem appears only after more complex DOM changes, such as moving the element to a different ancestor chain, then the real issue may be container discovery rather than axis support. The fixture helps separate those cases, but it does not replace checking the actual DOM path in your app.

The useful next step is to inspect the container ancestor at the moment the class changes. Ask whether the element still has a qualifying container, whether the named context is still present, and whether the axis you are querying is still supported by that container type. When those three answers line up, the query should match. When they do not, the browser is usually doing exactly what the spec says.

main.test.mjs / javascript
import test from 'node:test';
import assert from 'node:assert/strict';
import {
  createElement,
  appendChild,
  setContainerType,
  setContainerName,
  toggleContainerType,
  evaluateQuery,
} from './main.mjs';

test('size container matches both width and height queries', () => {
  const root = createElement({ width: 800, height: 600 });
  const child = createElement();
  appendChild(root, child);
  setContainerType(root, 'size');

  assert.equal(evaluateQuery(child, { feature: 'width', compare: '>', value: 700 }).matched, true);
  assert.equal(evaluateQuery(child, { feature: 'height', compare: '>=', value: 500 }).matched, true);
});

test('switching to inline-size stops block-axis matching but keeps inline-axis matching', () => {
  const root = createElement({ width: 800, height: 600 });
  const child = createElement();
  appendChild(root, child);
  setContainerType(root, 'size');
  toggleContainerType(root, 'inline-size');

  const inlineResult = evaluateQuery(child, { feature: 'width', compare: '>', value: 700 });
  assert.equal(inlineResult.matched, true);
  assert.equal(inlineResult.reason, undefined);

  const blockResult = evaluateQuery(child, { feature: 'height', compare: '>=', value: 500 });
  assert.equal(blockResult.matched, false);
  assert.equal(blockResult.reason, 'no-eligible-container');
});

test('name-only queries keep matching by name, but axis queries still depend on container-type', () => {
  const root = createElement({ width: 320, height: 240 });
  const child = createElement();
  appendChild(root, child);
  setContainerType(root, 'inline-size');
  setContainerName(root, 'sidebar');

  const nameOnly = evaluateQuery(child, { kind: 'name-only', name: 'sidebar' });
  assert.equal(nameOnly.matched, true);
  assert.equal(nameOnly.container, root);

  const blockQuery = evaluateQuery(child, { feature: 'height', compare: '>', value: 100 });
  assert.equal(blockQuery.matched, false);
  assert.equal(blockQuery.reason, 'no-eligible-container');

  setContainerType(root, 'size');
  const repaired = evaluateQuery(child, { feature: 'height', compare: '>', value: 100 });
  assert.equal(repaired.matched, true);
});

SourcesMDN: Container queries (opens a new tab)MDN: container-type (opens a new tab)

Share LinkedIn Email Subscribe

Discussion

Leave a comment

Comments appear after review. No email needed.

Image preview

Follow the blog

Choose email updates or your favorite RSS reader.

The weekly email

New articles on code, design, and AI. Fridays, only when there is something new.

By subscribing, you agree to weekly blog emails.
Confirm in your inbox. Unsubscribe anytime. Privacy

Use OpenRSS

Preview the feed, then choose a reader to subscribe.

Open in OpenRSS (opens a new tab)

Already have a reader?

Paste this link into your reader's Add feed option.

Prefer a reader? RSS does not require an email address. View XML feed