Skip to content

fix: resolve single box-shadow from runtime CSS variables#295

Open
YevheniiKotyrlo wants to merge 1 commit intonativewind:mainfrom
YevheniiKotyrlo:fix/runtime-box-shadow-variable-resolution
Open

fix: resolve single box-shadow from runtime CSS variables#295
YevheniiKotyrlo wants to merge 1 commit intonativewind:mainfrom
YevheniiKotyrlo:fix/runtime-box-shadow-variable-resolution

Conversation

@YevheniiKotyrlo
Copy link
Contributor

@YevheniiKotyrlo YevheniiKotyrlo commented Mar 9, 2026

Summary

Fix box-shadow shorthand resolution when CSS variables are resolved at runtime (not inlined at compile time). Currently, a single shadow from a runtime variable produces boxShadow: [] instead of the expected shadow object.

Problem

When a CSS variable holding a single box-shadow value is resolved at runtime, the shorthand handler receives a flat array of tokens like [0, 4, 6, -1, "#000"]. The handler incorrectly iterates each primitive individually — passing 0, 4, 6, etc. to the pattern matcher one at a time. No single primitive matches any shadow pattern, so all return undefined and get filtered out.

The runtime path is triggered when variables are:

  • Defined multiple times (e.g., :root + .dark for theme switching)
  • Only from @property (variable has an @property initial value but no class-level definition)
  • Not inlined (inlineVariables: false)

The compile-time path (single-definition variables that get inlined) is not affected — lightningcss parses the full shadow value correctly.

Reproduction

:root { --themed-shadow: 0 4px 6px -1px #000; }
.dark { --themed-shadow: 0 4px 6px -1px #fff; }
.test { box-shadow: var(--themed-shadow); }

Expected: boxShadow: [{ offsetX: 0, offsetY: 4, blurRadius: 6, spreadDistance: -1, color: "#000" }]
Actual: boxShadow: []

Solution

Detect whether the resolved args array is flat (single shadow) or nested (multiple shadows) before entering the multi-shadow handler:

  • Flat [0, 4, 6, -1, "#000"] → first element is a primitive → pass the entire array to the pattern handler as a single shadow
  • Nested [[0, 4, 6, -1, "#000"], [0, 1, 2, 0, "#333"]] → first element is an array → use existing flatMap logic for multiple shadows

The existing multi-shadow path (nested arrays from comma-separated box-shadow values) is unchanged. The flat array path also applies omitTransparentShadows and normalizeInsetValue for correct handling.

Why [[0, 4, 6, -1, "#000"]] (nested length-1) cannot happen

reduceParseUnparsed in declarations.ts (line 1075) always unwraps single groups:

return groups.length === 1 ? groups[0] : groups;

A single shadow always produces a flat array. Multiple shadows produce a nested array. There is no path through the compiler or runtime variable resolver that wraps a single shadow in an extra array layer.

Verification

  • yarn typecheck — pass
  • yarn lint — pass
  • yarn build — pass (ESM + CJS + DTS)
  • yarn test — 1014 passed, 3 failed (pre-existing babel plugin tests on main), 21 skipped (pre-existing)

Test architecture (38 tests: 34 box-shadow + 4 text-shadow)

Tests are structured with compile/runtime parity — each scenario is tested through both the compile-time and runtime variable resolution paths to prove they produce identical results.

Static CSS (10 tests) — single shadow (basic, color-first, negative offsets, no spread, no color), inset shadow (basic, color-first, no color, no spread), mixed inset + outset

Compile-time CSS variables (6 tests) — single nested var, deep nested vars, theme switching (multi-definition), inset via var, inset no-spread via var, transparent via var

Runtime variables (15 tests) — same scenarios as above with { inlineVariables: false }, plus: multi-shadow from separate vars, multi-shadow from single var, multi-shadow with transparent filtering

@property defaults (4 tests) — transparent defaults filtered, currentcolor platform object, Tailwind ring pattern (3 vars, 2 transparent) via both compile-time and runtime

Text-shadow regression (4 tests) — compile-time and runtime parity, proving text-shadow is unaffected (different resolver architecture — no flatMap)

Related

@danstepanov
Copy link
Member

@YevheniiKotyrlo Also please rebase this branch

@YevheniiKotyrlo YevheniiKotyrlo force-pushed the fix/runtime-box-shadow-variable-resolution branch from ffd33dd to c7e3eb3 Compare March 12, 2026 19:20
@YevheniiKotyrlo
Copy link
Contributor Author

Rebased on latest main (b0b35b1, includes merged #277). Single clean commit.

The fix now also applies normalizeInsetValue wrapping on the flat-array path (from the merged #277 inset shadow support), so runtime variable shadows with inset are handled correctly.

All quality gates pass (typecheck, lint, build with no unstaged files, test — 980 passed, 3 pre-existing babel failures same on main).

Copy link
Member

@danstepanov danstepanov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flat vs nested detection makes sense, and the tests cover the important cases (single, multi, transparent filtering, theme switching).

The !Array.isArray(args[0]) check assumes runtime variables always produce flat arrays for single shadows. Is there a case where a single shadow could come through as [[0, 4, 6, -1, "#000"]] (nested but length 1)? If so it'd hit the old flatMap path, which should still work but worth a quick sanity check. If you've verified that, we're good.

Also the branch needs a rebase.

@YevheniiKotyrlo YevheniiKotyrlo force-pushed the fix/runtime-box-shadow-variable-resolution branch from c7e3eb3 to 0b172b3 Compare March 13, 2026 12:52
@YevheniiKotyrlo
Copy link
Contributor Author

Good question. I traced through the compiler and runtime to verify — [[0, 4, 6, -1, "#000"]] (nested but length 1) cannot happen.

reduceParseUnparsed in declarations.ts line 1075 always unwraps single groups:

return groups.length === 1 ? groups[0] : groups;

So a single shadow always resolves to a flat array [0, 4, 6, -1, "#000"]. Multiple shadows produce [[...], [...]]. There's no path through the compiler or varResolver that wraps a single shadow in an extra array layer.

I've also expanded the test suite significantly — 34 box-shadow tests + 4 text-shadow tests now, structured as compile-time / runtime parity to prove both paths produce identical results. This covers single shadows, multi-shadow, inset, color-first, negative offsets, transparent filtering, @property defaults, and the Tailwind ring pattern (3 vars, 2 transparent).

Force pushing the rebased branch now (based on latest main).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants