Skip to content

Commit aee4d5d

Browse files
feat: add Tailwind CSS v4 shadow support and inset shadows
This PR adds two features for better Tailwind CSS v4 compatibility: 1. **Tailwind CSS v4 shadow variable defaults** - Tailwind v4 uses @Property to define initial-value for shadow CSS variables, but react-native-css doesn't support @Property - Added default values for tw-shadow, tw-inset-shadow, tw-ring-shadow, etc. to prevent "undefined variable" errors 2. **Inset shadow parsing support** - Added pattern matching for "inset" keyword in box-shadow values - Converts inset: "inset" to inset: true for React Native's boxShadow - Supports patterns: inset <x> <y> <blur> <spread> [color]
1 parent 008d479 commit aee4d5d

File tree

3 files changed

+163
-3
lines changed

3 files changed

+163
-3
lines changed

src/__tests__/native/box-shadow.test.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,115 @@ test("shadow values - multiple nested variables", () => {
9393
],
9494
});
9595
});
96+
97+
test("inset shadow - basic", () => {
98+
registerCSS(`
99+
.test { box-shadow: inset 0 2px 4px 0 #000; }
100+
`);
101+
102+
render(<View testID={testID} className="test" />);
103+
const component = screen.getByTestId(testID);
104+
105+
expect(component.props.style).toStrictEqual({
106+
boxShadow: [
107+
{
108+
inset: true,
109+
offsetX: 0,
110+
offsetY: 2,
111+
blurRadius: 4,
112+
spreadDistance: 0,
113+
color: "#000",
114+
},
115+
],
116+
});
117+
});
118+
119+
test("inset shadow - with color first", () => {
120+
registerCSS(`
121+
.test { box-shadow: inset #fb2c36 0 0 24px 0; }
122+
`);
123+
124+
render(<View testID={testID} className="test" />);
125+
const component = screen.getByTestId(testID);
126+
127+
expect(component.props.style).toStrictEqual({
128+
boxShadow: [
129+
{
130+
inset: true,
131+
color: "#fb2c36",
132+
offsetX: 0,
133+
offsetY: 0,
134+
blurRadius: 24,
135+
spreadDistance: 0,
136+
},
137+
],
138+
});
139+
});
140+
141+
test("inset shadow - without color inherits default", () => {
142+
registerCSS(`
143+
.test { box-shadow: inset 0 0 10px 5px; }
144+
`);
145+
146+
render(<View testID={testID} className="test" />);
147+
const component = screen.getByTestId(testID);
148+
149+
// Shadows without explicit color inherit the default text color (__rn-css-color)
150+
expect(component.props.style.boxShadow).toHaveLength(1);
151+
expect(component.props.style.boxShadow[0]).toMatchObject({
152+
inset: true,
153+
offsetX: 0,
154+
offsetY: 0,
155+
blurRadius: 10,
156+
spreadDistance: 5,
157+
});
158+
// Color is inherited from platform default (PlatformColor)
159+
expect(component.props.style.boxShadow[0].color).toBeDefined();
160+
});
161+
162+
test("mixed inset and regular shadows", () => {
163+
registerCSS(`
164+
.test { box-shadow: 0 4px 6px -1px #000, inset 0 2px 4px 0 #fff; }
165+
`);
166+
167+
render(<View testID={testID} className="test" />);
168+
const component = screen.getByTestId(testID);
169+
170+
expect(component.props.style).toStrictEqual({
171+
boxShadow: [
172+
{
173+
offsetX: 0,
174+
offsetY: 4,
175+
blurRadius: 6,
176+
spreadDistance: -1,
177+
color: "#000",
178+
},
179+
{
180+
inset: true,
181+
offsetX: 0,
182+
offsetY: 2,
183+
blurRadius: 4,
184+
spreadDistance: 0,
185+
color: "#fff",
186+
},
187+
],
188+
});
189+
});
190+
191+
test("Tailwind v4 shadow variables - transparent color #0000", () => {
192+
// Tailwind v4 uses --tw-shadow etc with @property initial-value of transparent
193+
registerCSS(`
194+
:root {
195+
--tw-shadow: 0 0 0 0 #0000;
196+
}
197+
.test { box-shadow: var(--tw-shadow); }
198+
`);
199+
200+
render(<View testID={testID} className="test" />);
201+
const component = screen.getByTestId(testID);
202+
203+
// The shadow is parsed correctly with #0000 color
204+
// Note: filtering of transparent shadows happens in omitTransparentShadows
205+
// which checks for exact "#0000" or "transparent" strings
206+
expect(component.props.style.boxShadow).toBeDefined();
207+
});

src/native-internal/root.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,26 @@ rootVariables("__rn-css-color").set([
4242
],
4343
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4444
] as any);
45+
46+
/**
47+
* Tailwind CSS v4 shadow variable defaults.
48+
*
49+
* Tailwind v4 uses @property to define initial-value for shadow CSS variables,
50+
* but react-native-css doesn't support @property declarations.
51+
*
52+
* These provide fallback values that match Tailwind's defaults:
53+
* - Transparent shadows (0 0 0 0 #0000) are filtered out by omitTransparentShadows
54+
* - This prevents "undefined variable" errors when shadow utilities are used
55+
*
56+
* @see https://github.com/tailwindlabs/tailwindcss/discussions/16772
57+
*/
58+
// VariableValue[] where each VariableValue is [StyleDescriptor] tuple
59+
// The inner [0, 0, 0, 0, "#0000"] is a StyleDescriptor[] (shadow values)
60+
const transparentShadow: VariableValue[] = [[[0, 0, 0, 0, "#0000"]]];
61+
rootVariables("tw-shadow").set(transparentShadow);
62+
rootVariables("tw-shadow-color").set([["initial"]]);
63+
rootVariables("tw-inset-shadow").set(transparentShadow);
64+
rootVariables("tw-inset-shadow-color").set([["initial"]]);
65+
rootVariables("tw-ring-shadow").set(transparentShadow);
66+
rootVariables("tw-inset-ring-shadow").set(transparentShadow);
67+
rootVariables("tw-ring-offset-shadow").set(transparentShadow);

src/native/styles/shorthands/box-shadow.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,25 @@ const offsetX = ["offsetX", "number"] as const;
99
const offsetY = ["offsetY", "number"] as const;
1010
const blurRadius = ["blurRadius", "number"] as const;
1111
const spreadDistance = ["spreadDistance", "number"] as const;
12-
// const inset = ["inset", "string"] as const;
12+
// Match the literal string "inset" - the array type checks if value is in array
13+
const inset = ["inset", ["inset"]] as const;
1314

1415
const handler = shorthandHandler(
1516
[
17+
// Standard patterns (without inset)
1618
[offsetX, offsetY, blurRadius, spreadDistance],
1719
[offsetX, offsetY, blurRadius, spreadDistance, color],
1820
[color, offsetX, offsetY],
1921
[color, offsetX, offsetY, blurRadius, spreadDistance],
2022
[offsetX, offsetY, color],
2123
[offsetX, offsetY, blurRadius, color],
24+
// Inset patterns - "inset" keyword at the beginning
25+
// Matches: inset <offsetX> <offsetY> <blur> <spread>
26+
[inset, offsetX, offsetY, blurRadius, spreadDistance],
27+
// Matches: inset <offsetX> <offsetY> <blur> <spread> <color>
28+
[inset, offsetX, offsetY, blurRadius, spreadDistance, color],
29+
// Matches: inset <color> <offsetX> <offsetY> <blur> <spread>
30+
[inset, color, offsetX, offsetY, blurRadius, spreadDistance],
2231
],
2332
[],
2433
"object",
@@ -41,8 +50,10 @@ export const boxShadow: StyleFunctionResolver = (
4150
if (shadows === undefined) {
4251
return;
4352
} else {
44-
return omitTransparentShadows(
45-
handler(resolveValue, shadows, get, options),
53+
return normalizeInsetValue(
54+
omitTransparentShadows(
55+
handler(resolveValue, shadows, get, options),
56+
),
4657
);
4758
}
4859
})
@@ -69,3 +80,17 @@ function omitTransparentShadows(style: unknown) {
6980

7081
return style;
7182
}
83+
84+
/**
85+
* Convert inset: "inset" to inset: true for React Native boxShadow.
86+
*
87+
* The shorthand handler matches the literal "inset" string and assigns it as the value.
88+
* React Native's boxShadow expects inset to be a boolean.
89+
*/
90+
function normalizeInsetValue(style: unknown) {
91+
if (typeof style === "object" && style && "inset" in style) {
92+
return { ...style, inset: true };
93+
}
94+
95+
return style;
96+
}

0 commit comments

Comments
 (0)