Repository navigation
fix(native): read a runtime shadow the way the compiler does - #489
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
YevheniiKotyrlo wants to merge 2 commits into
YevheniiKotyrlo wants to merge 2 commits into
Conversation
A box-shadow, text-shadow or drop-shadow() whose value is only known at runtime was matched against a fixed list of token orders. A spelling the list missed was dropped, `inset` without a colour was taken for the colour, and an omitted blur, spread or colour was left out where the compiler writes 0 and currentcolor. A colourless drop-shadow() became an empty filter entry, which React Native's processFilter throws on, and a lone length reached React Native as a number, which processBoxShadow throws on. All three now read their tokens through one reader: the lengths adjacent, a colour and `inset` on either side, in the counts lightningcss accepts for each property, with every shadow of a list read and an invalid one rejecting the whole value.
Contributor
Author
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 11:47
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 21:13
…r drop shadow does A written value that is not a shadow is invalid, and browsers ignore the declaration. A variable holding one leaves the declaration invalid at computed-value time, which browsers make unset (css-variables-1 §3.1). The reader can tell neither from the other, so every shadow it rejects sets nothing, and each test row asserts what browsers compute.
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 23:02
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
On native, a
box-shadow,text-shadowordrop-shadow()whose value is only known at runtime - a variable defined more than once, one with only an@propertyinitial value, or any variable underinlineVariables: false- is matched against a fixed list of token orders. CSS lets the colour andinsetsit on either side of the lengths, so most spellings miss the list:2px 2pxcurrentcolor[]#123456 2px 2px 4px[]inset 2px 2pxcolor: "inset", which React Native rejects, dropping every shadow in the list2px 2px #123456 inset[]2px 2px #123456, 4px 4px2px 2px 4px 1pxcurrentcolortext-shadow: 2px 2px 4pxdrop-shadow(2px 2px)[{}], whichprocessFilterthrows onThe runtime shadow also leaves out an omitted blur and spread where the compiler writes
0, an omittedtext-shadowcolour isblackwhere the compiler writescurrentcolor, and a lone length (--shadow: 2px) reaches React Native asboxShadow: 2, whichprocessBoxShadowthrows on. Tailwind'stext-shadow-md text-shadow-red-500draws no text shadow on native, because the colour utility moves the list to runtime.Solution
The three properties read their tokens through one reader,
shorthands/_shadow.ts: the lengths adjacent, a colour andinseton either side of them, in the counts lightningcss accepts for each property - four lengths andinsetforbox-shadow, four lengths fortext-shadow, three fordrop-shadow(). An omitted blur and spread are0and an omitted colour iscurrentcolor, so the runtime shadow is the object the compiler writes. Every shadow of a list is read, a variable that resolved to part of a shadow is spliced into it, and one invalid shadow makes the whole value invalid, which sets nothing: browsers ignore a written invalid shadow, and a variable holding one leaves the declaration invalid at computed-value time, which the reader cannot tell apart.text-shadowkeeps the first shadow, as the compiler does. Nothing usesshorthandHandler's"object"return any more, so it goes.Tests
src/__tests__/native/shadow-grammar.test.tsxruns every order of colour, lengths andinset, a list, a list held in another variable, a variable holding part of a shadow, and the invalid spellings - held in a variable forbox-shadowanddrop-shadow(), which leave no shadow, and written fortext-shadow, which leaves the earlier rule's - each with variables inlined and resolved at runtime, and with the sheet injected as compiled and as the JSON a device receives. Every expected value is what Chromium 153, Firefox 155 and WebKit 26.6 compute for the same CSS. Onmain122 of the 269 fail. The four runtime cases inbox-shadow.test.tsxthat pinned the old shape, the "known limitation" among them, now expect the compiler's, andeffects.test.tsxadds Tailwind'sinset-shadow-smandtext-shadow-mdwith a text-shadow colour, both failing onmain.Verification
On Ubuntu with the workflow's steps - build, lint, typecheck,
yarn test,node --test node/__tests__/*.mjs, both library builds, the web export, the unstaged-changes checks and commitlint - everything passes merged with #500. Without it,yarn testfails a random Tailwind case in a full run, the harness flake #500 removes.Device evidence, before and after: #489 (comment)
Not run: the iOS and Android example builds.
Unchanged: a negative blur, which lightningcss accepts, still passes through on both paths, and
text-shadow: nonestill sets nothing. A shadow split across two variables that each hold lengths (var(--offsets) var(--blur-spread)) still reads as two shadows, because the compiler stores it as the same list asvar(--a), var(--b).Also unchanged, outside the reader: a variable holding what is not a text shadow leaves an earlier rule's text shadow in place, where browsers make the declaration
unset(css-variables-1 §3.1) - #391 does that for every property. And a written invalidbox-shadowordrop-shadow()clears an earlier rule's value where browsers ignore the declaration, because the runtime holds the key with a placeholder until the value resolves and drops it when the value is invalid, and a filter list leaves out an entry its function rejects.Merge order
It shares lines with #391 (
native/styles/functions/filters.ts), #392 and #413 (native/styles/shorthands/box-shadow.ts); whichever lands second rebases. #413 moves the transparent-colour check intoutilities/transparent-color.ts, which this file then uses, and #391 resets a cleared shorthand insideshorthandHandler, whichtext-shadowno longer goes through, so whichever of the two lands second carries that reset intotext-shadow.ts.