Skip to content

fix: evaluate aspect-ratio media and container queries - #425

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/container-query-defects
Open

YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/container-query-defects

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On main, aspect-ratio never matches, in @media or @container. parseMediaFeatureValue has no arm for a <ratio>, so the condition compiles to nothing and its block is dropped, and the media evaluator never measures the screen's aspect ratio.

Solution

  • A <ratio> compiles to its quotient, and the media evaluator measures width over height, as the container evaluator already does.
  • Chromium, Firefox and WebKit compare a ratio over 0 as infinite, 0/0 included, so (max-aspect-ratio: 1/0) matches and (min-aspect-ratio: 1/0) does not. A device receives the stylesheet as JSON, which writes Infinity as null, so such a ratio compiles to Number.MAX_VALUE.

Tests

src/__tests__/native/aspect-ratio.test.tsx checks ten conditions against a 400x200 screen and a 400x200 container, and that a ratio over 0 survives the JSON round trip. 11 of its 21 cases fail on main.

Verification

Every ci.yml job but the iOS build, on Ubuntu with Node 22.18.0: green, with yarn test at 1356 passed.

Device evidence, before and after: #425 (comment)

Related

Range pairs moved to #426, and logical container sizes to #480. This PR and #426 both edit native/conditions/media-query.ts; whichever lands second, I re-cut.

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Outdated: #426 is now re-written on top of #425, so the two no longer conflict.

The original note

Cross-referencing #425 and #426, which conflict — flagging it now rather than at merge time.

They are independent siblings (git merge-base is main; neither is an ancestor of the other) and git merge-tree reports content conflicts in four files: src/compiler/compiler.ts, src/compiler/container-query.ts, src/__tests__/compiler/media-query.test.ts, src/__tests__/native/container-queries.test.tsx.

The hazard is in the [] (interval) arm of testContainerQuery, because a plausible resolution silently reverts work:

Taking #426's arm wholesale therefore un-fixes #425's range container queries. Taking #425's wholesale loses #426's three-valued handling, where false is the one answer a negation turns into a match.

The resolution that is correct in both directions evaluates the interval and returns UNKNOWN only when a bound is unorderable.

One more line worth keeping whichever way it lands: #426's boolean-context arm uses isTruthyFeatureValue (Number.isFinite(value) && value !== 0), and the plain value !== 0 form admits NaN, Infinity and -Infinity. That is reachable — @container (aspect-ratio) compiles to ["!!","aspect-ratio"] and the feature value is width / height, so a zero-height container yields Infinity and the weaker form matches where it should not.

A two-case test pins both halves, and neither branch passes it alone:

  1. @container box (400px < width < 800px) over a 500x100 container must match. (fix: evaluate aspect-ratio media and container queries #425 ✅ · fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown #426 ❌)
  2. @container box (10em < width < 20em) and its not (…) twin must both fail over the same container — an unorderable bound is unknown, not false. (fix: evaluate aspect-ratio media and container queries #425 ❌, its negation matches · fix: evaluate range pairs, and answer a condition the runtime cannot decide with unknown #426 ✅)

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Outdated: #426 is now re-written on top of #425, so the two no longer conflict.

The original note

Correction to my note above — I undercounted the conflict set, and the omission was the load-bearing file.

git merge-tree --write-tree fix/container-query-defects fix/media-condition-semantics reports seven conflicted files, not four:

src/compiler/compiler.ts
src/compiler/container-query.ts
src/compiler/media-query.ts                  ← omitted above
src/native/conditions/container-query.ts     ← omitted above
src/native/conditions/media-query.ts         ← omitted above
src/__tests__/compiler/media-query.test.ts
src/__tests__/native/container-queries.test.tsx

src/native/conditions/container-query.ts is the file whose []-arm resolution my note describes, so leaving it off the list made the note harder to act on rather than merely incomplete.

A second resolution needs the same care, in src/compiler/container-query.ts's "not" arm. main has return query ? ["!", query] : undefined. #425 changes parseContainerCondition to return a discriminated union, so its arm becomes query.type === "condition" ? ["!", query.condition] : undefined — which DROPS an unrepresentable operand. #426 keeps main's return type and writes return ["!", query ?? ["?"]], so an unrepresentable operand negates to unknown (MQ5 §3.1).

Taking #426's arm onto #425's types does not compile — query is an object there and never nullish. Taking #425's arm silently reverts #426's fix. The resolution that keeps both is #425's discriminant with #426's fallback:

["!", query.type === "condition" ? query.condition : (["?"] as MediaCondition)]

And one more, in src/native/conditions/compare.ts. #426 introduces MediaFeatureOperand = Exclude<StyleDescriptor, undefined> | null and a parseMediaFeatureOperand that can hand the comparator null; compare.ts exists only on #425, where both operands are typed StyleDescriptor, which excludes null. On the merge those call sites are a compile error rather than a silent null === null → true — so it self-flags, but the fix is to widen both operands and guard left !== null before the equality shortcut.

YevheniiKotyrlo added a commit to YevheniiKotyrlo/react-native-css that referenced this pull request Aug 16, 2026
…unanswerable value

`parseMediaFeatureOperand`'s doc comment lists what has no compile-time answer
as `env()`, a ratio, an unsupported `calc()`. On this branch that is true, and
it stops being true the moment nativewind#425 lands: there a reducible ratio compiles to
its quotient and only a degenerate one stays unanswerable.

Naming the ratio more precisely does not fix it. A restrictive qualifier — "a
ratio the compiler cannot reduce to a finite quotient" — implies a non-empty
complement, and on this branch the complement is empty: every ratio, `16/9` and
bare `1` included, answers `undefined`, because `case "ratio"` falls through to
`case "env"`. So the qualified form is truth-conditionally fine and tells a
reader of THIS branch something false about it. There is no wording that names
ratios here and is right both before and after nativewind#425.

Dropping the ratio is not a narrower list, it is an honest one, because the
enumeration was never the case table it reads as. `parseLength` refuses 43 units
by name — every physical unit, every font-relative unit but `em`/`rem`, every
viewport and container variant but bare `vw`/`vh` — so `(min-width: 1lh)`
already answers `null` here with no `calc()` anywhere in it, unlisted. The two
members kept are the two verified stable: `env()` and an unsupported `calc()`
answer `null` on this branch and after nativewind#425 alike.

`such as` is what stops the next reader trusting the list as exhaustive, which
is the failure this comment already had. The ratio's own explanation belongs at
the hop that owns the fact, and nativewind#425 puts it there, in `case "ratio"`.

Comments only.
@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Only the control paints. The compiler drops every aspect-ratio condition, so the container ratio, the screen ratio and the infinite ratio all stay outlines.

FIXED — All four bars paint. A container's and the screen's aspect ratio is width over height, and a ratio over 0 compares as infinite.

before — 3.1.0-rc.0 after — with this PR

Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. before is the published 3.1.0-rc.0 plus what my test app needs from my other patches to run: the native and native-internal subpath exports, PR 459, cascade-layer ranking, and a shim that compiles a class-qualified :root inert. after adds the rest of my open PRs, this one included. One of them shows on every screen: PR 417 narrows a var()-valued font-family to one family, so the after sets its text in the theme's face where the before falls back to the platform's.

@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/container-query-defects branch from 387fa75 to 769f216 Compare October 11, 2026 12:35
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(native): evaluate container and media conditions correctly on both axes and every operator fix(native): evaluate range pairs, aspect ratios and logical container sizes Oct 11, 2026
parseMediaFeatureValue had no arm for a <ratio>, so every aspect-ratio
condition compiled to nothing and its block was dropped, and the media
evaluator never measured the screen's aspect ratio.

A ratio over 0 compiles to Number.MAX_VALUE: every engine compares it as
infinite, and the stylesheet reaches a device as JSON, which writes
Infinity as null.
@YevheniiKotyrlo
YevheniiKotyrlo force-pushed the fix/container-query-defects branch from 769f216 to 31518ac Compare October 11, 2026 18:15
@YevheniiKotyrlo YevheniiKotyrlo changed the title fix(native): evaluate range pairs, aspect ratios and logical container sizes fix: evaluate aspect-ratio media and container queries Oct 11, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant