Skip to content

fix: draw no text shadow for text-shadow: none - #495

Open
YevheniiKotyrlo wants to merge 4 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/text-shadow-none
Open

YevheniiKotyrlo wants to merge 4 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/text-shadow-none

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

text-shadow: none never removes a text shadow. lightningcss hands none over unparsed, so it resolves at runtime, and the runtime reader answers it with nothing: .shadowed { text-shadow: 1px 1px red } .plain { text-shadow: none } keeps the red shadow on class="shadowed plain", and a nested Text with text-shadow: none keeps its parent's. Chromium 153, Firefox 155 and WebKit 26.6 draw neither.

Solution

React Native has no none, and a nested Text inherits every shadow prop its own style leaves unset, so none becomes a transparent shadow with no offset or blur, which draws nothing and outranks the parent's.

The compiler writes that for a literal none: lightningcss leaves it unparsed, and resolved at runtime it would land after a later rule's text shadow, because a runtime-parsed declaration resolves after its element's others. The runtime reader writes the same for a variable holding none, and both read it from NO_TEXT_SHADOW in src/utilities/text-shadow.ts.

A variable holding something that is not a text shadow still leaves the earlier value in place, where CSS makes the declaration unset; that holds for every property, so it is not part of this change. Until #474 lands, a variable holding none in an earlier rule still beats a later rule's shadow, as any text-shadow: var() does.

Tests

src/__tests__/native/text-shadow-none.test.tsx covers none in a later rule, on a child under a shadowed parent and in a variable resolved at runtime, a later rule's shadow over an earlier none, and a second element's none after a shadow was written over the first's. 4 of the 5 fail on #489, and each of three mutations fails its own case: a literal none resolved at runtime, a reader that does not know none, and the offset compiled as one object, which the later shadow then writes into.

Merge order

Stacked on #489, whose text-shadow reader this changes, so #489 lands first.

It also shares lines with #389 (compiler/declarations.ts, utilities/index.ts), #391 (compiler/declarations.ts, native/styles/functions/filters.ts, utilities/index.ts), #392 (native/styles/shorthands/box-shadow.ts), #393 (compiler/declarations.ts), #413 (compiler/declarations.ts, native/styles/shorthands/box-shadow.ts), #417 (compiler/declarations.ts), #420 (compiler/declarations.ts, utilities/index.ts), #491 (compiler/declarations.ts, utilities/index.ts), #492 (compiler/declarations.ts, utilities/index.ts), #494 (compiler/declarations.ts, utilities/index.ts); whichever lands second rebases.

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.

Device evidence, before and after: #495 (comment)

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.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Every line casts the red shadow, the nested word included: text-shadow: none reaches the style as nothing, so the earlier rule's shadow and the parent's stay.

FIXED — Only the control and the word Parent cast the red shadow. text-shadow: none removes an earlier rule's shadow, the one a variable holding none replaces, and the one a nested text takes from its parent.

before — 3.1.0-rc.0 after — with this PR

Android 16 emulator (API 36, GLES on the host GPU), 1140×2400 @ 480dpi, both frames from one run. The variable row's --shadow is declared twice in the stylesheet, so it resolves at runtime. before is the published 3.1.0-rc.0 plus what my test app needs from my other patches to run: the native and native-internal subpath exports, PR 459, cascade-layer ranking, and a shim that compiles a class-qualified :root inert. after adds the rest of my open PRs, this one included. One of them shows on every screen: PR 417 narrows a var()-valued font-family to one family, so the after sets its text in the theme's face where the before falls back to the platform's.

@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 15:00
…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
YevheniiKotyrlo marked this pull request as draft October 16, 2026 22:44
lightningcss hands `text-shadow: none` over unparsed, so it resolves at
runtime, where the reader answered `none` with nothing: an earlier
rule's text shadow survived where CSS removes it, and a nested Text kept
its parent's. React Native has no `none`, so the reader writes a
transparent shadow with no offset or blur, which draws nothing.

The compiler also marked every runtime-parsed shorthand to resolve after
the element's other declarations, which a value that reads no variable
does not need; resolved there, an earlier rule's `none` would beat a
later rule's shadow. Such a value now resolves in its cascade position.
Resolving a runtime-parsed shorthand in place lost what its delay gives a
CSS-wide keyword: the placeholder that replaces an earlier rule's value,
so `box-shadow: initial` after `box-shadow: 1px 1px red` kept the shadow.
The delay stays. `text-shadow: none`, which lightningcss leaves unparsed,
compiles instead to the no-shadow the runtime reader writes for a
variable holding `none`, so it keeps its cascade position.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review October 16, 2026 23:02

This branch has not been deployed

No deployments
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.

1 participant