From eb8b8cd73017ea65ee5ae313cda3b7489db5b9d6 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 16 Oct 2026 15:59:32 +0300 Subject: [PATCH 1/8] fix(compiler): convert absolute length units to pixels css-values-4 fixes in, cm, mm, Q, pt and pc against the inch (1in = 96px), so each has an exact pixel value. parseLength warned and dropped all six, so a declaration, a variable and a media query holding one set nothing on native. --- src/__tests__/native/absolute-units.test.tsx | 62 ++++++++++++++++++++ src/compiler/declarations.ts | 3 +- src/utilities/absolute-units.ts | 9 +++ src/utilities/index.ts | 1 + 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/native/absolute-units.test.tsx create mode 100644 src/utilities/absolute-units.ts diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx new file mode 100644 index 00000000..236ae38f --- /dev/null +++ b/src/__tests__/native/absolute-units.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react-native"; +import { View } from "react-native-css/components/View"; +import { registerCSS, testID } from "react-native-css/jest"; + +// css-values-4 §6.2: 1in = 2.54cm = 25.4mm = 101.6Q = 72pt = 6pc = 96px +const UNITS = [ + ["1in", 96], + ["2.54cm", 96], + ["10mm", 37.7953], + ["4Q", 3.7795], + ["3pt", 4], + ["1pc", 16], +] as const; + +function styleFor(className: string): unknown { + render(); + return screen.getByTestId(testID).props.style; +} + +test.each(UNITS)("width: %s is %d pixels", (length, pixels) => { + registerCSS(`.subject { width: ${length}; }`); + + expect(styleFor("subject")).toStrictEqual({ width: pixels }); +}); + +test.each(UNITS)( + "a variable holding %s resolves to %d pixels at runtime", + (length, pixels) => { + registerCSS(` + .theme { --width: ${length}; } + .subject { --width: ${length}; width: var(--width); } + `); + + expect(styleFor("theme subject")).toStrictEqual({ width: pixels }); + }, +); + +test("an absolute length converts when rem is not inlined", () => { + registerCSS(`.subject { width: 1in; }`, { inlineRem: false }); + + expect(styleFor("subject")).toStrictEqual({ width: 96 }); +}); + +test("a media query reads an absolute length", () => { + registerCSS(` + @media (min-width: 4in) { .subject { width: 1pc; } } + @media (max-width: 4in) { .subject { height: 1pc; } } + `); + + expect(styleFor("subject")).toStrictEqual({ width: 16 }); +}); + +test("a shorthand converts each of its lengths", () => { + registerCSS(`.subject { margin: 1in 1pc; }`); + + expect(styleFor("subject")).toStrictEqual({ + marginTop: 96, + marginRight: 16, + marginBottom: 96, + marginLeft: 16, + }); +}); diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 9ca2aab8..f4e01f3d 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -36,7 +36,7 @@ import type { UnresolvedColor, } from "lightningcss"; -import { isStyleFunction } from "../utilities"; +import { ABSOLUTE_UNIT_PIXELS, isStyleFunction } from "../utilities"; import { isMappingProperty } from "./atRules"; import type { StyleDescriptor, @@ -1461,6 +1461,7 @@ export function parseLength( case "q": case "pt": case "pc": + return round(length.value * ABSOLUTE_UNIT_PIXELS[length.unit]); case "ex": case "rex": case "ch": diff --git a/src/utilities/absolute-units.ts b/src/utilities/absolute-units.ts new file mode 100644 index 00000000..42d0e27c --- /dev/null +++ b/src/utilities/absolute-units.ts @@ -0,0 +1,9 @@ +/** css-values-4 §6.2 fixes each absolute length unit against the inch (1in = 96px) rather than the device. */ +export const ABSOLUTE_UNIT_PIXELS = { + cm: 96 / 2.54, + in: 96, + mm: 96 / 25.4, + pc: 16, + pt: 96 / 72, + q: 96 / 101.6, +} as const satisfies Record; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 0c95da63..96b0ee30 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,4 @@ export * from "./specificity"; export * from "./style-descriptor"; export * from "./dot-notation.types"; +export * from "./absolute-units"; From 4b6ae17746549e12d6e5b79c551c1fa2065df75f Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 20:27:43 +0300 Subject: [PATCH 2/8] fix(compiler): clamp absolute lengths through the px branch --- src/__tests__/native/absolute-units.test.tsx | 38 +++++++++++++++----- src/compiler/declarations.ts | 23 ++++++++++-- src/utilities/absolute-units.ts | 9 ----- src/utilities/index.ts | 1 - 4 files changed, 50 insertions(+), 21 deletions(-) delete mode 100644 src/utilities/absolute-units.ts diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx index 236ae38f..20f362d5 100644 --- a/src/__tests__/native/absolute-units.test.tsx +++ b/src/__tests__/native/absolute-units.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from "@testing-library/react-native"; import { View } from "react-native-css/components/View"; import { registerCSS, testID } from "react-native-css/jest"; -// css-values-4 §6.2: 1in = 2.54cm = 25.4mm = 101.6Q = 72pt = 6pc = 96px +// https://www.w3.org/TR/css-values-4/#absolute-lengths: 1in = 2.54cm = 25.4mm = 101.6Q = 72pt = 6pc = 96px const UNITS = [ ["1in", 96], ["2.54cm", 96], @@ -26,12 +26,11 @@ test.each(UNITS)("width: %s is %d pixels", (length, pixels) => { test.each(UNITS)( "a variable holding %s resolves to %d pixels at runtime", (length, pixels) => { - registerCSS(` - .theme { --width: ${length}; } - .subject { --width: ${length}; width: var(--width); } - `); + registerCSS(`.subject { --width: ${length}; width: var(--width); }`, { + inlineVariables: false, + }); - expect(styleFor("theme subject")).toStrictEqual({ width: pixels }); + expect(styleFor("subject")).toStrictEqual({ width: pixels }); }, ); @@ -41,10 +40,10 @@ test("an absolute length converts when rem is not inlined", () => { expect(styleFor("subject")).toStrictEqual({ width: 96 }); }); -test("a media query reads an absolute length", () => { +test("a media query reads an absolute length at 96 pixels an inch", () => { registerCSS(` - @media (min-width: 4in) { .subject { width: 1pc; } } - @media (max-width: 4in) { .subject { height: 1pc; } } + @media (min-width: 7.8in) { .subject { width: 1pc; } } + @media (min-width: 7.9in) { .subject { height: 1pc; } } `); expect(styleFor("subject")).toStrictEqual({ width: 16 }); @@ -60,3 +59,24 @@ test("a shorthand converts each of its lengths", () => { marginLeft: 16, }); }); + +test.each(["width", "border-radius"])( + "an infinite absolute %s clamps as an infinite px one does", + (property) => { + registerCSS(` + .inch { ${property}: calc(infinity * 1in); } + .pixel { ${property}: calc(infinity * 1px); } + `); + + expect(styleFor("inch")).toStrictEqual(styleFor("pixel")); + }, +); + +test("an inlined variable holding an infinite absolute length clamps as px does", () => { + registerCSS(` + .inch { --inch-radius: calc(infinity * 1pt); border-radius: var(--inch-radius); } + .pixel { --pixel-radius: calc(infinity * 1px); border-radius: var(--pixel-radius); } + `); + + expect(styleFor("inch")).toStrictEqual(styleFor("pixel")); +}); diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index f4e01f3d..fa48697e 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -20,6 +20,7 @@ import type { GradientItemFor_DimensionPercentageFor_LengthValue, Length, LengthPercentageOrAuto, + LengthUnit, LengthValue, LineDirection, LineHeight, @@ -36,7 +37,7 @@ import type { UnresolvedColor, } from "lightningcss"; -import { ABSOLUTE_UNIT_PIXELS, isStyleFunction } from "../utilities"; +import { isStyleFunction } from "../utilities"; import { isMappingProperty } from "./atRules"; import type { StyleDescriptor, @@ -61,6 +62,21 @@ type Parser = ( // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => StyleDescriptor | void; +type AbsoluteLengthUnit = Extract< + LengthUnit, + "in" | "cm" | "mm" | "q" | "pt" | "pc" +>; + +/** Pixels in each absolute unit: https://www.w3.org/TR/css-values-4/#absolute-lengths */ +const absoluteUnitPixels: Readonly> = { + cm: 96 / 2.54, + in: 96, + mm: 96 / 25.4, + pc: 16, + pt: 96 / 72, + q: 96 / 101.6, +}; + const propertyRename: Record = { "background-image": "experimental_backgroundImage", // React Native has no border-inline-* props, but ships the equivalent @@ -1461,7 +1477,10 @@ export function parseLength( case "q": case "pt": case "pc": - return round(length.value * ABSOLUTE_UNIT_PIXELS[length.unit]); + return parseLength( + { unit: "px", value: length.value * absoluteUnitPixels[length.unit] }, + builder, + ); case "ex": case "rex": case "ch": diff --git a/src/utilities/absolute-units.ts b/src/utilities/absolute-units.ts deleted file mode 100644 index 42d0e27c..00000000 --- a/src/utilities/absolute-units.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** css-values-4 §6.2 fixes each absolute length unit against the inch (1in = 96px) rather than the device. */ -export const ABSOLUTE_UNIT_PIXELS = { - cm: 96 / 2.54, - in: 96, - mm: 96 / 25.4, - pc: 16, - pt: 96 / 72, - q: 96 / 101.6, -} as const satisfies Record; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 96b0ee30..0c95da63 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,4 +1,3 @@ export * from "./specificity"; export * from "./style-descriptor"; export * from "./dot-notation.types"; -export * from "./absolute-units"; From a67e8faeb4ed0581d4bb285e339419ffbef8b19a Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 21:22:03 +0300 Subject: [PATCH 3/8] test(compiler): pin absolute lengths in any case and sign, in transforms, shadows and font sizes --- src/__tests__/native/absolute-units.test.tsx | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx index 20f362d5..1b466f08 100644 --- a/src/__tests__/native/absolute-units.test.tsx +++ b/src/__tests__/native/absolute-units.test.tsx @@ -34,6 +34,33 @@ test.each(UNITS)( }, ); +test.each([ + ["width: 1IN", { width: 96 }], + ["width: 2.54CM", { width: 96 }], + ["margin-left: -1pc", { marginLeft: -16 }], + ["margin-top: 0pt", { marginTop: 0 }], + ["transform: translateX(1in)", { transform: [{ translateX: 96 }] }], + [ + "box-shadow: 3pt 6pt 1pc -3pt #000", + { + boxShadow: [ + { + offsetX: 4, + offsetY: 8, + blurRadius: 16, + spreadDistance: -4, + color: "#000", + }, + ], + }, + ], + ["font-size: 12pt", { fontSize: 16 }], +] as const)("%s converts to pixels", (declaration, style) => { + registerCSS(`.subject { ${declaration}; }`); + + expect(styleFor("subject")).toStrictEqual(style); +}); + test("an absolute length converts when rem is not inlined", () => { registerCSS(`.subject { width: 1in; }`, { inlineRem: false }); From 9e86555b49534dd35b82450ae6a6dd19d7996f7a Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 21:59:24 +0300 Subject: [PATCH 4/8] test(compiler): pin absolute lengths in runtime calc(), @property and container queries --- src/__tests__/native/absolute-units.test.tsx | 44 +++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx index 1b466f08..10ad2594 100644 --- a/src/__tests__/native/absolute-units.test.tsx +++ b/src/__tests__/native/absolute-units.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react-native"; +import { fireEvent, render, screen } from "@testing-library/react-native"; import { View } from "react-native-css/components/View"; import { registerCSS, testID } from "react-native-css/jest"; @@ -38,7 +38,7 @@ test.each([ ["width: 1IN", { width: 96 }], ["width: 2.54CM", { width: 96 }], ["margin-left: -1pc", { marginLeft: -16 }], - ["margin-top: 0pt", { marginTop: 0 }], + ["margin-top: -0.75pt", { marginTop: -1 }], ["transform: translateX(1in)", { transform: [{ translateX: 96 }] }], [ "box-shadow: 3pt 6pt 1pc -3pt #000", @@ -107,3 +107,43 @@ test("an inlined variable holding an infinite absolute length clamps as px does" expect(styleFor("inch")).toStrictEqual(styleFor("pixel")); }); + +test("a runtime calc() adds an absolute length", () => { + registerCSS(`.subject { --width: 10px; width: calc(var(--width) + 1in); }`, { + inlineVariables: false, + }); + + expect(styleFor("subject")).toStrictEqual({ width: 106 }); +}); + +test("an @property initial value converts", () => { + registerCSS(` + @property --width { syntax: ""; inherits: false; initial-value: 1in; } + .subject { width: var(--width); } + `); + + expect(styleFor("subject")).toStrictEqual({ width: 96 }); +}); + +test.each([ + [390, { width: 16 }], + [380, undefined], +])( + "a container %d pixels wide matches (min-width: 4in): %o", + (containerWidth, style) => { + registerCSS(` + .container { container-name: box; } + @container box (min-width: 4in) { .child { width: 1pc; } } + `); + render( + + + , + ); + fireEvent(screen.getByTestId("container"), "layout", { + nativeEvent: { layout: { width: containerWidth, height: 100 } }, + }); + + expect(screen.getByTestId(testID).props.style).toStrictEqual(style); + }, +); From 62047f72e260229b27c97fd8c7d0c63035934a7b Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 22:21:19 +0300 Subject: [PATCH 5/8] refactor(compiler): list the absolute units once, in their table --- src/compiler/declarations.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index fa48697e..b9676b82 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -62,20 +62,15 @@ type Parser = ( // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => StyleDescriptor | void; -type AbsoluteLengthUnit = Extract< - LengthUnit, - "in" | "cm" | "mm" | "q" | "pt" | "pc" ->; - /** Pixels in each absolute unit: https://www.w3.org/TR/css-values-4/#absolute-lengths */ -const absoluteUnitPixels: Readonly> = { +const absoluteUnitPixels = { cm: 96 / 2.54, in: 96, mm: 96 / 25.4, pc: 16, pt: 96 / 72, q: 96 / 101.6, -}; +} as const satisfies Partial>; const propertyRename: Record = { "background-image": "experimental_backgroundImage", From bad71dc050f5cbdc3146fec75c5c28ffdaff4b3c Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 22:21:33 +0300 Subject: [PATCH 6/8] test(compiler): give the container case a size container --- src/__tests__/native/absolute-units.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx index 10ad2594..10de53be 100644 --- a/src/__tests__/native/absolute-units.test.tsx +++ b/src/__tests__/native/absolute-units.test.tsx @@ -129,10 +129,10 @@ test.each([ [390, { width: 16 }], [380, undefined], ])( - "a container %d pixels wide matches (min-width: 4in): %o", + "a container %d pixels wide against (min-width: 4in) styles its child %o", (containerWidth, style) => { registerCSS(` - .container { container-name: box; } + .container { container-name: box; container-type: inline-size; } @container box (min-width: 4in) { .child { width: 1pc; } } `); render( From 8aeeb7bed03cf98b2aa391c6f54827247de57cbd Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 22:50:30 +0300 Subject: [PATCH 7/8] refactor(compiler): keep the absolute unit table beside parseLength --- src/compiler/declarations.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index b9676b82..88019584 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -62,16 +62,6 @@ type Parser = ( // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => StyleDescriptor | void; -/** Pixels in each absolute unit: https://www.w3.org/TR/css-values-4/#absolute-lengths */ -const absoluteUnitPixels = { - cm: 96 / 2.54, - in: 96, - mm: 96 / 25.4, - pc: 16, - pt: 96 / 72, - q: 96 / 101.6, -} as const satisfies Partial>; - const propertyRename: Record = { "background-image": "experimental_backgroundImage", // React Native has no border-inline-* props, but ships the equivalent @@ -1425,6 +1415,16 @@ export function parseLengthDeclaration( return parseLength(declaration.value, builder); } +/** Pixels in each absolute unit: https://www.w3.org/TR/css-values-4/#absolute-lengths */ +const absoluteUnitPixels = { + cm: 96 / 2.54, + in: 96, + mm: 96 / 25.4, + pc: 16, + pt: 96 / 72, + q: 96 / 101.6, +} as const satisfies Partial>; + export function parseLength( length: | number From 0795609226f8476a4a744ce5e5425d048b22f9be Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 19 Oct 2026 22:50:45 +0300 Subject: [PATCH 8/8] test(compiler): pin infinite absolute lengths, name the container rows, set the media window --- src/__tests__/native/absolute-units.test.tsx | 36 +++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/__tests__/native/absolute-units.test.tsx b/src/__tests__/native/absolute-units.test.tsx index 10de53be..0d8f5e4b 100644 --- a/src/__tests__/native/absolute-units.test.tsx +++ b/src/__tests__/native/absolute-units.test.tsx @@ -2,8 +2,10 @@ import { fireEvent, render, screen } from "@testing-library/react-native"; import { View } from "react-native-css/components/View"; import { registerCSS, testID } from "react-native-css/jest"; +import { dimensions } from "../../native/reactivity"; + // https://www.w3.org/TR/css-values-4/#absolute-lengths: 1in = 2.54cm = 25.4mm = 101.6Q = 72pt = 6pc = 96px -const UNITS = [ +const absoluteLengths = [ ["1in", 96], ["2.54cm", 96], ["10mm", 37.7953], @@ -17,13 +19,13 @@ function styleFor(className: string): unknown { return screen.getByTestId(testID).props.style; } -test.each(UNITS)("width: %s is %d pixels", (length, pixels) => { +test.each(absoluteLengths)("width: %s is %d pixels", (length, pixels) => { registerCSS(`.subject { width: ${length}; }`); expect(styleFor("subject")).toStrictEqual({ width: pixels }); }); -test.each(UNITS)( +test.each(absoluteLengths)( "a variable holding %s resolves to %d pixels at runtime", (length, pixels) => { registerCSS(`.subject { --width: ${length}; width: var(--width); }`, { @@ -68,6 +70,7 @@ test("an absolute length converts when rem is not inlined", () => { }); test("a media query reads an absolute length at 96 pixels an inch", () => { + dimensions.set({ ...dimensions.get(), width: 750 }); registerCSS(` @media (min-width: 7.8in) { .subject { width: 1pc; } } @media (min-width: 7.9in) { .subject { height: 1pc; } } @@ -87,15 +90,23 @@ test("a shorthand converts each of its lengths", () => { }); }); -test.each(["width", "border-radius"])( +test.each([ + ["width", "width"], + ["border-radius", "borderRadius"], +] as const)( "an infinite absolute %s clamps as an infinite px one does", - (property) => { + (property, styleProperty) => { registerCSS(` .inch { ${property}: calc(infinity * 1in); } .pixel { ${property}: calc(infinity * 1px); } `); - expect(styleFor("inch")).toStrictEqual(styleFor("pixel")); + expect(styleFor("inch")).toStrictEqual({ + [styleProperty]: 9007199254740990, + }); + expect(styleFor("pixel")).toStrictEqual({ + [styleProperty]: 9007199254740990, + }); }, ); @@ -105,7 +116,8 @@ test("an inlined variable holding an infinite absolute length clamps as px does" .pixel { --pixel-radius: calc(infinity * 1px); border-radius: var(--pixel-radius); } `); - expect(styleFor("inch")).toStrictEqual(styleFor("pixel")); + expect(styleFor("inch")).toStrictEqual({ borderRadius: 9007199254740991 }); + expect(styleFor("pixel")).toStrictEqual({ borderRadius: 9007199254740991 }); }); test("a runtime calc() adds an absolute length", () => { @@ -126,11 +138,11 @@ test("an @property initial value converts", () => { }); test.each([ - [390, { width: 16 }], - [380, undefined], -])( - "a container %d pixels wide against (min-width: 4in) styles its child %o", - (containerWidth, style) => { + [390, "styles its child", { width: 16 }], + [380, "leaves its child unstyled", undefined], +] as const)( + "a container %d pixels wide against (min-width: 4in) %s", + (containerWidth, _outcome, style) => { registerCSS(` .container { container-name: box; container-type: inline-size; } @container box (min-width: 4in) { .child { width: 1pc; } }