Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/__tests__/native/color-mix.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

expect(component.props.style).toStrictEqual({
backgroundColor: "#00000080",
});
});
36 changes: 32 additions & 4 deletions src/compiler/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down