diff --git a/src/__tests__/compiler/compiler.test.tsx b/src/__tests__/compiler/compiler.test.tsx
index 7a9fbea..d7561ab 100644
--- a/src/__tests__/compiler/compiler.test.tsx
+++ b/src/__tests__/compiler/compiler.test.tsx
@@ -448,3 +448,121 @@ test("simplifies rem", () => {
],
});
});
+
+describe("CSS-wide color keywords", () => {
+ const stylesheetFor = (value: string) =>
+ compile(`.child { color: ${value}; }`).stylesheet();
+
+ test("compiles to the inherited-color variable instead of being dropped", () => {
+ // lightningcss emits `color: inherit` as an UnparsedProperty (the keyword is
+ // not a CssColor), which parseUnparsed used to drop. Per CSS Color,
+ // `currentcolor` used as the value of `color` is defined as `inherit`, so it
+ // resolves to the same inherited-color variable. The ABSENCE of a `v` entry
+ // is the no-self-reference guarantee — publishing this value as its own
+ // --__rn-css-color would seed a circular var(--__rn-css-color).
+ expect(stylesheetFor("inherit")).toStrictEqual({
+ s: [
+ [
+ "child",
+ [
+ {
+ s: [1, 1],
+ d: [[[{}, "var", "__rn-css-color"], "color", 1]],
+ dv: 1,
+ },
+ ],
+ ],
+ ],
+ });
+ });
+
+ test("inherit and currentcolor compile identically (CSS Color spec identity)", () => {
+ expect(stylesheetFor("inherit")).toStrictEqual(
+ stylesheetFor("currentcolor"),
+ );
+ });
+
+ test("currentcolor still resolves to the inherited-color variable (unchanged)", () => {
+ // The token/ident branch that hunk 1 restructured also carries currentcolor;
+ // this pins that currentcolor keeps compiling to the same lookup, and — like
+ // inherit — never self-publishes a `v`.
+ expect(stylesheetFor("currentcolor")).toStrictEqual({
+ s: [
+ [
+ "child",
+ [
+ {
+ s: [1, 1],
+ d: [[[{}, "var", "__rn-css-color"], "color", 1]],
+ dv: 1,
+ },
+ ],
+ ],
+ ],
+ });
+ });
+
+ test("a normal color still publishes --__rn-css-color to descendants", () => {
+ expect(stylesheetFor("red")).toStrictEqual({
+ s: [
+ [
+ "child",
+ [
+ {
+ s: [1, 1],
+ d: [{ color: "#f00" }],
+ v: [["__rn-css-color", "#f00"]],
+ },
+ ],
+ ],
+ ],
+ });
+ });
+
+ test("inherit on a non-color property is still dropped (no inheritance context)", () => {
+ expect(
+ compile(`.child { font-size: inherit; }`).stylesheet(),
+ ).toStrictEqual({});
+ });
+
+ test("color: initial is still dropped (different semantics, out of scope)", () => {
+ expect(stylesheetFor("initial")).toStrictEqual({});
+ });
+
+ test("color: unset resolves like inherit (unset on an inherited property is inherit)", () => {
+ // Per CSS Cascade, `unset` computes to `inherit` on inherited properties,
+ // and `color` is inherited — so it maps to the same inherited-color variable.
+ expect(stylesheetFor("unset")).toStrictEqual(stylesheetFor("inherit"));
+ });
+
+ test("keyword matching is case-insensitive (INHERIT)", () => {
+ // CSS-wide keywords are case-insensitive; lightningcss does not fold case.
+ expect(stylesheetFor("INHERIT")).toStrictEqual(stylesheetFor("inherit"));
+ });
+
+ test("currentColor (camelCase) resolves like currentcolor", () => {
+ // The spelling React/JS authors reach for; it is valid, case-insensitive CSS.
+ expect(stylesheetFor("currentColor")).toStrictEqual(
+ stylesheetFor("currentcolor"),
+ );
+ });
+
+ test("currentcolor resolves on a non-color property too (border-color)", () => {
+ expect(
+ compile(`.child { border-color: currentcolor; }`).stylesheet(),
+ ).toStrictEqual({
+ s: [
+ [
+ "child",
+ [
+ {
+ s: [1, 1],
+ d: [[[{}, "var", "__rn-css-color"], "borderColor", 1]],
+ dv: 1,
+ },
+ ],
+ ],
+ ],
+ });
+ });
+});
diff --git a/src/__tests__/native/colors.test.tsx b/src/__tests__/native/colors.test.tsx
index 6a8c725..b67a971 100644
--- a/src/__tests__/native/colors.test.tsx
+++ b/src/__tests__/native/colors.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react-native";
+import { Text } from "react-native-css/components/Text";
import { View } from "react-native-css/components/View";
import { registerCSS, testID } from "react-native-css/jest";
@@ -165,3 +166,105 @@ describe("currentcolor", () => {
});
});
});
+
+describe("inherit", () => {
+ test("color: inherit resolves to the parent's color", () => {
+ registerCSS(`
+ .parent { color: red; }
+ .child { color: inherit; }
+ `);
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("child").props.style).toStrictEqual({
+ color: "#f00",
+ });
+ });
+
+ test("text-inherit: a child Text inherits its parent's color", () => {
+ // The shape that surfaced the bug: a labelled button whose label renders
+ // React Native's default color (black) on native instead of the button's
+ // foreground color, while web inherits correctly.
+ registerCSS(`
+ .button { color: white; }
+ .label { color: inherit; }
+ `);
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("label").props.style).toStrictEqual({
+ color: "#fff",
+ });
+ });
+
+ test("inherit chains through an inheriting ancestor without breaking the chain", () => {
+ // The middle node inherits and must NOT republish a circular
+ // --__rn-css-color, or the grandchild would fail to resolve the color.
+ registerCSS(`
+ .parent { color: red; }
+ .mid { color: inherit; }
+ .child { color: inherit; }
+ `);
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("mid").props.style).toStrictEqual({
+ color: "#f00",
+ });
+ expect(screen.getByTestId("child").props.style).toStrictEqual({
+ color: "#f00",
+ });
+ });
+
+ test("inherit follows the nearest colored ancestor", () => {
+ registerCSS(`
+ .outer { color: red; }
+ .inner { color: blue; }
+ .child { color: inherit; }
+ `);
+
+ render(
+
+
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("child").props.style).toStrictEqual({
+ color: "#00f",
+ });
+ });
+
+ test("color: unset inherits the parent's color, same as inherit", () => {
+ // `unset` computes to `inherit` on inherited properties, and color is one.
+ registerCSS(`
+ .parent { color: red; }
+ .child { color: unset; }
+ `);
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId("child").props.style).toStrictEqual({
+ color: "#f00",
+ });
+ });
+});
diff --git a/src/__tests__/vendor/tailwind/typography.test.tsx b/src/__tests__/vendor/tailwind/typography.test.tsx
index 00d184b..c286a3c 100644
--- a/src/__tests__/vendor/tailwind/typography.test.tsx
+++ b/src/__tests__/vendor/tailwind/typography.test.tsx
@@ -322,9 +322,17 @@ describe("Typography - Text Color", () => {
});
});
test("text-inherit", async () => {
+ // Per CSS Color, `inherit` on the `color` property is defined as
+ // `currentcolor`, so text-inherit resolves to the platform label color —
+ // identical to text-current above — instead of being dropped with a warning.
expect(await renderCurrentTest()).toStrictEqual({
- props: {},
- warnings: { values: { color: "inherit" } },
+ props: {
+ style: {
+ color: {
+ semantic: ["label", "labelColor"],
+ },
+ },
+ },
});
});
});
diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts
index 1301364..7591a6f 100644
--- a/src/compiler/declarations.ts
+++ b/src/compiler/declarations.ts
@@ -950,10 +950,15 @@ export function parseUnparsedDeclaration(
}
if (property === "color") {
+ // Publish the resolved color to descendants as --__rn-css-color — the
+ // variable `currentcolor` and `color: inherit` both resolve against. Skip
+ // when the value IS that same lookup (e.g. `color: inherit`) so an
+ // inheriting rule never seeds a circular `--__rn-css-color:
+ // var(--__rn-css-color)` that would break resolution for its own subtree.
if (
!isStyleFunction(value) ||
value[1] !== "var" ||
- value[2] !== "-css-color"
+ value[2] !== "__rn-css-color"
) {
builder.addDescriptor("--__rn-css-color", value);
}
@@ -1263,11 +1268,30 @@ export function parseUnparsed(
return;
}
- if (value === "inherit" || value === "initial") {
+ // CSS-wide keywords and `currentcolor` are case-insensitive.
+ const keyword = value.toLowerCase();
+
+ // Per CSS Color, `currentcolor` as the value of `color` is defined as
+ // `inherit`; and per CSS Cascade, `unset` on an inherited property
+ // (`color` is inherited) computes to `inherit` too. So `currentcolor`
+ // (valid on any property) and `inherit` / `unset` on `color` all
+ // resolve to the inherited-color variable every color rule publishes
+ // to its subtree (see parseUnparsedDeclaration).
+ if (
+ keyword === "currentcolor" ||
+ ((keyword === "inherit" || keyword === "unset") &&
+ property === "color")
+ ) {
+ return [{}, "var", "__rn-css-color"] as const;
+ }
+
+ // `inherit` and `initial` on any other property have no per-property
+ // resolution context here — drop with a warning. `unset` on a
+ // non-color property (= `initial` there) and `revert` /
+ // `revert-layer` keep their existing fall-through handling below.
+ if (keyword === "inherit" || keyword === "initial") {
builder.addWarning("value", value);
return;
- } else if (value === "currentcolor") {
- return [{}, "var", "__rn-css-color"] as const;
}
if (value === "true") {