From 3148e172c41db7614b641b4eeaefccad68a659b8 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Wed, 17 Jun 2026 10:59:47 -0400 Subject: [PATCH] fix: coerce NaN Lab/OKLab channels to 0 in parseColor color-mix() resolved at compile time by lightningcss can yield NaN chromaticity channels for degenerate mixes (e.g. mixing black with transparent in oklab). Passing NaN to colorjs.io produced an invalid color string like "#NaNNaNNaN80" that React Native silently discards, so utilities such as Tailwind's bg-black/50 rendered with no background. Treat NaN channels as 0 (a missing component per CSS Color 4) for the lab/lch/oklab/oklch cases, so the example resolves to #00000080. Fixes #317 --- src/__tests__/native/color-mix.test.tsx | 21 +++++++++++++++ src/compiler/declarations.ts | 36 ++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/__tests__/native/color-mix.test.tsx b/src/__tests__/native/color-mix.test.tsx index bed3cea2..f581d1f1 100644 --- a/src/__tests__/native/color-mix.test.tsx +++ b/src/__tests__/native/color-mix.test.tsx @@ -45,3 +45,24 @@ test("color-mix() - oklch", () => { backgroundColor: "rgba(231, 0, 11, 0.5)", }); }); + +test("color-mix() - black with transparent (NaN oklab channels)", () => { + // lightningcss resolves this at compile time to oklab(0 NaN NaN / 0.5): + // black is oklab [l=0, a=0, b=0] and transparent has no chromaticity, so the + // a/b channels degenerate to NaN. Without coercing NaN to 0 the color + // serializes to "#NaNNaNNaN80", which React Native silently discards. + // This is what Tailwind's `bg-black/50` compiles to. + registerCSS( + `.test { + background-color: color-mix(in oklab, #000 50%, transparent); + } + `, + ); + + render(); + const component = screen.getByTestId(testID); + + expect(component.props.style).toStrictEqual({ + backgroundColor: "#00000080", + }); +}); diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 4031778a..13013642 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -1612,6 +1612,18 @@ export function parseColorDeclaration( ); } +/** + * Lab/LCH/OKLab/OKLCH channels can be `NaN` when lightningcss resolves a + * degenerate `color-mix()` at compile time (e.g. mixing black with + * `transparent` in oklab yields `NaN` for the a/b chromaticity channels). + * Passing `NaN` to colorjs.io produces an invalid string such as + * `#NaNNaNNaN80`, which React Native silently discards. Per CSS Color 4 a + * missing component is treated as `0`, so coerce `NaN` to `0`. + */ +function nanToZero(value: number): number { + return Number.isNaN(value) ? 0 : value; +} + export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) { if (typeof cssColor === "string") { if (namedColors.has(cssColor)) { @@ -1666,28 +1678,44 @@ export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) { case "lab": color = new Color({ space: cssColor.type, - coords: [cssColor.l, cssColor.a, cssColor.b], + coords: [ + nanToZero(cssColor.l), + nanToZero(cssColor.a), + nanToZero(cssColor.b), + ], alpha: cssColor.alpha, }); break; case "lch": color = new Color({ space: cssColor.type, - coords: [cssColor.l, cssColor.c, cssColor.h], + coords: [ + nanToZero(cssColor.l), + nanToZero(cssColor.c), + nanToZero(cssColor.h), + ], alpha: cssColor.alpha, }); break; case "oklab": color = new Color({ space: cssColor.type, - coords: [cssColor.l, cssColor.a, cssColor.b], + coords: [ + nanToZero(cssColor.l), + nanToZero(cssColor.a), + nanToZero(cssColor.b), + ], alpha: cssColor.alpha, }); break; case "oklch": color = new Color({ space: cssColor.type, - coords: [cssColor.l, cssColor.c, cssColor.h], + coords: [ + nanToZero(cssColor.l), + nanToZero(cssColor.c), + nanToZero(cssColor.h), + ], alpha: cssColor.alpha, }); break;