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
112 changes: 112 additions & 0 deletions src/__tests__/native/box-shadow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,115 @@ test("shadow values - multiple nested variables", () => {
],
});
});

test("inset shadow - basic", () => {
registerCSS(`
.test { box-shadow: inset 0 2px 4px 0 #000; }
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

expect(component.props.style).toStrictEqual({
boxShadow: [
{
inset: true,
offsetX: 0,
offsetY: 2,
blurRadius: 4,
spreadDistance: 0,
color: "#000",
},
],
});
});

test("inset shadow - with color first", () => {
registerCSS(`
.test { box-shadow: inset #fb2c36 0 0 24px 0; }
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

expect(component.props.style).toStrictEqual({
boxShadow: [
{
inset: true,
color: "#fb2c36",
offsetX: 0,
offsetY: 0,
blurRadius: 24,
spreadDistance: 0,
},
],
});
});

test("inset shadow - without color inherits default", () => {
registerCSS(`
.test { box-shadow: inset 0 0 10px 5px; }
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

// Shadows without explicit color inherit the default text color (__rn-css-color)
expect(component.props.style.boxShadow).toHaveLength(1);
expect(component.props.style.boxShadow[0]).toMatchObject({
inset: true,
offsetX: 0,
offsetY: 0,
blurRadius: 10,
spreadDistance: 5,
});
// Color is inherited from platform default (PlatformColor)
expect(component.props.style.boxShadow[0].color).toBeDefined();
});

test("mixed inset and regular shadows", () => {
registerCSS(`
.test { box-shadow: 0 4px 6px -1px #000, inset 0 2px 4px 0 #fff; }
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

expect(component.props.style).toStrictEqual({
boxShadow: [
{
offsetX: 0,
offsetY: 4,
blurRadius: 6,
spreadDistance: -1,
color: "#000",
},
{
inset: true,
offsetX: 0,
offsetY: 2,
blurRadius: 4,
spreadDistance: 0,
color: "#fff",
},
],
});
});

test("Tailwind v4 shadow variables - transparent color #0000", () => {
// Tailwind v4 uses --tw-shadow etc with @property initial-value of transparent
registerCSS(`
:root {
--tw-shadow: 0 0 0 0 #0000;
}
.test { box-shadow: var(--tw-shadow); }
`);

render(<View testID={testID} className="test" />);
const component = screen.getByTestId(testID);

// The shadow is parsed correctly with #0000 color
// Note: filtering of transparent shadows happens in omitTransparentShadows
// which checks for exact "#0000" or "transparent" strings
expect(component.props.style.boxShadow).toBeDefined();
});
23 changes: 23 additions & 0 deletions src/native-internal/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,26 @@ rootVariables("__rn-css-color").set([
],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any);

/**
* 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 declarations.
*
* These provide fallback values that match Tailwind's defaults:
* - Transparent shadows (0 0 0 0 #0000) are filtered out by omitTransparentShadows
* - This prevents "undefined variable" errors when shadow utilities are used
*
* @see https://github.com/tailwindlabs/tailwindcss/discussions/16772
*/
// VariableValue[] where each VariableValue is [StyleDescriptor] tuple
// The inner [0, 0, 0, 0, "#0000"] is a StyleDescriptor[] (shadow values)
const transparentShadow: VariableValue[] = [[[0, 0, 0, 0, "#0000"]]];
rootVariables("tw-shadow").set(transparentShadow);
rootVariables("tw-shadow-color").set([["initial"]]);
rootVariables("tw-inset-shadow").set(transparentShadow);
rootVariables("tw-inset-shadow-color").set([["initial"]]);
rootVariables("tw-ring-shadow").set(transparentShadow);
rootVariables("tw-inset-ring-shadow").set(transparentShadow);
rootVariables("tw-ring-offset-shadow").set(transparentShadow);
31 changes: 28 additions & 3 deletions src/native/styles/shorthands/box-shadow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,25 @@ const offsetX = ["offsetX", "number"] as const;
const offsetY = ["offsetY", "number"] as const;
const blurRadius = ["blurRadius", "number"] as const;
const spreadDistance = ["spreadDistance", "number"] as const;
// const inset = ["inset", "string"] as const;
// Match the literal string "inset" - the array type checks if value is in array
const inset = ["inset", ["inset"]] as const;

const handler = shorthandHandler(
[
// Standard patterns (without inset)
[offsetX, offsetY, blurRadius, spreadDistance],
[offsetX, offsetY, blurRadius, spreadDistance, color],
[color, offsetX, offsetY],
[color, offsetX, offsetY, blurRadius, spreadDistance],
[offsetX, offsetY, color],
[offsetX, offsetY, blurRadius, color],
// Inset patterns - "inset" keyword at the beginning
// Matches: inset <offsetX> <offsetY> <blur> <spread>
[inset, offsetX, offsetY, blurRadius, spreadDistance],
// Matches: inset <offsetX> <offsetY> <blur> <spread> <color>
[inset, offsetX, offsetY, blurRadius, spreadDistance, color],
// Matches: inset <color> <offsetX> <offsetY> <blur> <spread>
[inset, color, offsetX, offsetY, blurRadius, spreadDistance],
],
[],
"object",
Expand All @@ -41,8 +50,10 @@ export const boxShadow: StyleFunctionResolver = (
if (shadows === undefined) {
return;
} else {
return omitTransparentShadows(
handler(resolveValue, shadows, get, options),
return normalizeInsetValue(
omitTransparentShadows(
handler(resolveValue, shadows, get, options),
),
);
}
})
Expand All @@ -69,3 +80,17 @@ function omitTransparentShadows(style: unknown) {

return style;
}

/**
* Convert inset: "inset" to inset: true for React Native boxShadow.
*
* The shorthand handler matches the literal "inset" string and assigns it as the value.
* React Native's boxShadow expects inset to be a boolean.
*/
function normalizeInsetValue(style: unknown) {
if (typeof style === "object" && style && "inset" in style) {
return { ...style, inset: true };
}

return style;
}