diff --git a/src/__tests__/native/aspect-ratio.test.tsx b/src/__tests__/native/aspect-ratio.test.tsx
new file mode 100644
index 00000000..a6194988
--- /dev/null
+++ b/src/__tests__/native/aspect-ratio.test.tsx
@@ -0,0 +1,60 @@
+import { act, fireEvent, render, screen } from "@testing-library/react-native";
+import { compile } from "react-native-css/compiler";
+import { View } from "react-native-css/components/View";
+import { registerCSS } from "react-native-css/jest";
+
+import { dimensions } from "../../native/reactivity";
+
+// Each condition against a 400x200 box, whose aspect ratio is 2
+const cases = [
+ ["(aspect-ratio: 2/1)", true],
+ ["(aspect-ratio: 3/1)", false],
+ ["(min-aspect-ratio: 2/1)", true],
+ ["(min-aspect-ratio: 201/100)", false],
+ ["(max-aspect-ratio: 2/1)", true],
+ ["(max-aspect-ratio: 199/100)", false],
+ ["(aspect-ratio > 1)", true],
+ ["(max-aspect-ratio: 1/0)", true],
+ ["(min-aspect-ratio: 1/0)", false],
+ ["(min-aspect-ratio: 0/0)", false],
+] as const;
+
+test.each(cases)("@media %s matches: %s", (condition, matches) => {
+ registerCSS(`
+ .child { width: 10px; }
+ @media ${condition} { .child { width: 37px; } }
+ `);
+ act(() => {
+ dimensions.set({ ...dimensions.get(), width: 400, height: 200 });
+ });
+ render();
+
+ expect(screen.getByTestId("child")).toHaveStyle({ width: matches ? 37 : 10 });
+});
+
+test.each(cases)("@container %s matches: %s", (condition, matches) => {
+ registerCSS(`
+ .parent { container-type: size; }
+ .child { width: 10px; }
+ @container ${condition} { .child { width: 37px; } }
+ `);
+ render(
+
+
+ ,
+ );
+ fireEvent(screen.getByTestId("parent"), "layout", {
+ nativeEvent: { layout: { x: 0, y: 0, width: 400, height: 200 } },
+ });
+
+ expect(screen.getByTestId("child")).toHaveStyle({ width: matches ? 37 : 10 });
+});
+
+test("a ratio over 0 survives the JSON a device receives the stylesheet as", () => {
+ const stylesheet = compile(
+ "@media (min-aspect-ratio: 1/0) { .child { color: red; } }",
+ ).stylesheet();
+
+ expect(stylesheet.s).toHaveLength(1);
+ expect(JSON.parse(JSON.stringify(stylesheet))).toStrictEqual(stylesheet);
+});
diff --git a/src/compiler/media-query.ts b/src/compiler/media-query.ts
index 1408adc0..d09a29c7 100644
--- a/src/compiler/media-query.ts
+++ b/src/compiler/media-query.ts
@@ -174,7 +174,11 @@ export function parseMediaFeatureValue(
value.value satisfies never;
return undefined;
}
- case "ratio":
+ case "ratio": {
+ const [numerator, denominator] = value.value;
+ // Every engine compares a ratio over 0 as infinite, 0/0 included, and JSON cannot carry Infinity
+ return denominator === 0 ? Number.MAX_VALUE : numerator / denominator;
+ }
case "env":
}
diff --git a/src/native/conditions/media-query.ts b/src/native/conditions/media-query.ts
index afd51250..7dbf0568 100644
--- a/src/native/conditions/media-query.ts
+++ b/src/native/conditions/media-query.ts
@@ -75,6 +75,9 @@ function testComparison(mediaQuery: MediaCondition, get: Getter): Boolean {
case "height":
left = get(vh);
break;
+ case "aspect-ratio":
+ left = get(vw) / get(vh);
+ break;
case "resolution":
left = PixelRatio.get();
break;