Repository navigation
fix(native): read a runtime shadow the way the compiler does - #489
Open
YevheniiKotyrlo wants to merge 1 commit into
Open
YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
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
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 23:02
YevheniiKotyrlo
marked this pull request as draft
October 23, 2026 17:47
YevheniiKotyrlo
force-pushed
the
fix/runtime-shadow-grammar
branch
from
October 23, 2026 17:52
68dbe5b to
0eb4e34
Compare
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. One invalid shadow makes the whole value invalid, which sets nothing: browsers ignore a written one, and a variable holding one leaves the declaration invalid at computed-value time (css-variables-1 §3.1). shorthandHandler loses its "object" return, which nothing uses any more, and its branch that let an unresolved style function fill any slot: the handler resolves its tokens before it matches them, so no input reached it. The Tailwind shadow tests run with the --tw-* variables inlined and resolved at runtime, and a border test resolves the handler's remaining "shorthandObject" return.
YevheniiKotyrlo
force-pushed
the
fix/runtime-shadow-grammar
branch
from
October 23, 2026 18:01
0eb4e34 to
c48fef2
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 23, 2026 18:48
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, and so does its branch that let an unresolved style function fill any slot: the handler resolves its tokens before it matches them, so no input reached it, and the shadows its comment named no longer use the handler.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.effects.test.tsxruns Tailwind's box and text shadow cases with the--tw-*variables inlined and resolved at runtime, through aninlineVariablesoption the test harness passes to the compiler. Onmain,inset-shadow-smandtext-shadow-mdwith a text-shadow colour fail either way, andtext-shadow-mdfails with its variables resolved at runtime.border.test.tsxresolvesborder, the oneshorthandHandleruser left on its"shorthandObject"return, at runtime.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, on the head and merged intomainwith #477, without which a Tailwind case can fail at random. The iOS dev build passes on Xcode 27.1 with the example'sexpoat 57.0.31;ci.ymlbuilds with Xcode 26.6, and on 27.1main'sexpo57.0.22 fails in its prebuiltExpoModulesJSI.Device evidence, before and after, on Android and an iPhone: #489 (comment)
Reading a runtime shadow costs no more than on
main: over four spellings both read, the head's median time per resolution is lower for each, withinmain's run-to-run spread.Unchanged: a negative blur, which lightningcss accepts, still passes through on both paths, and
text-shadow: nonestill sets nothing, which #495 fixes. 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. A written invalidbox-shadowordrop-shadow()clears an earlier rule's value where browsers ignore the declaration, and a filter list leaves out an entry its function rejects; #501 keeps the earlier value in both.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.Two others change what this PR's rows expect, and whichever lands second moves them. #392 seeds
currentcolorwith a concrete colour, whereshadow-grammar.test.tsx'scurrentColorand twobox-shadow.test.tsxcases expectmain'sPlatformColor. #501, on #391 and #474, leavesfilterunset for a variable holding an invalid drop shadow, where the rows expect[], and with variables inlined keeps an earlier shadow for a variable holding an invalid one, since the inliner hands it over as a written value, until #503 keeps it a substitution.#495 is stacked on this.