Repository navigation
fix: ignore a filter list with a function it cannot take, as written or from a var() - #501
Open
YevheniiKotyrlo wants to merge 29 commits into
Open
YevheniiKotyrlo wants to merge 29 commits into
YevheniiKotyrlo wants to merge 29 commits into
Conversation
`color: inherit`, `unset` and `currentcolor` compile to a read of the colour every `color` rule publishes, and a colour that reads that channel is not published back into it. While an element computes its own `color`, the read resolves in the scope it inherited, because css-color-4 makes `currentcolor` the inherited colour there; on every other property it stays the element's own colour. nativewind#451 ordered the element's own variables before inherited ones, so the inherited read is now explicit rather than a consequence of lookup order.
The compiler and the resolver each asked whether a var() reads the inherited colour, under one name with two meanings. The shallow question is now `isInheritedColorLookup` beside the variable it names, and the compiler's walk calls it for each var() it meets. Each constraint is stated in one line, citing the sections that state it.
No caller passes parseUnparsed a string or a number: with either removed from the parameter type, tsc reports no call site, and the test suite never takes either branch. The string branch's `currentcolor` arm was a third spelling of the inherited-colour lookup that nothing reached.
Each expected value is what Chromium, Firefox and WebKit compute for the same CSS. The cases that pinned a divergence are gone: a light-dark() descendant in dark mode, a custom property holding `inherit` that is not inlined, and the string relative colour syntax resolves to. `::selection` is left to the pseudo-element mapping, and `::placeholder` also reads the element's own colour.
A CSS-wide keyword is a value only on its own (css-values-4 §2.1), so the compiler resolves one where it is a declaration's whole value or a var() fallback's, and rejects one inside a longer value. `revert` and `revert-layer` roll back to `unset`, as React Native has no user-agent or user origin. `unset` is `inherit` on `color` and on a custom property and `initial` elsewhere, and `initial` clears the property to React Native's default. A custom property left to inherit publishes nothing, so a read finds its ancestor's value, and its warnings now name it. A shorthand cleared by a keyword resets every longhand it sets, and a cleared filter is none. An element's inherited colour comes from its last rule that publishes one, so a later `color` that inherits no longer leaves an earlier rule's colour for its subtree, and a pseudo-element's colour stays its own.
The first pass prints the stylesheet before the second parses it, so no white space from a comment reaches the keyword, and the filter for it was unreachable.
A shorthand parsed at runtime was registered with an empty argument when its value failed to parse, which cleared an earlier rule's value; browsers ignore an invalid declaration, so it is left out. The CSS-wide keyword cases join the existing keyword suite.
… none A filter or transform list that a var() fills with `none`, a cleared value or `initial` is `none` alone or invalid at computed-value time, and either way the property is `none`; the resolvers returned the keyword inside the list.
…rive cleared Only a shorthand that returns its longhands as an object receives a cleared argument: animation wraps its argument in a list, and box-shadow and drop-shadow handle it before the handler.
…he declaration A keyword is a value only on its own, so one inside a function, a colour or a list makes the whole declaration invalid; rejecting the token alone left `box-shadow: 1px 1px inherit` as a shadow and `blur(inherit)` as an empty blur. A var() or env() defers its value, and a custom property's value is any tokens, so neither is checked.
…CSS-wide keyword whole These three parse their own value, so a keyword inside it was dropped as a token and the rest applied: `object-position: left inherit` set `left`. They now share the declaration's keyword decision, so the declaration is invalid and an earlier rule's value stands, as it does in Chromium 153, Firefox 155 and WebKit 26.6.
… the function is A fallback replaces its var() or env(), so a CSS-wide keyword in it applies only when the function is the declaration's whole value (css-variables-1 §3). Inside a longer value the keyword leaves the declaration invalid at computed-value time, yet it was resolved everywhere: `color-mix(in srgb, var(--missing, inherit), blue)` mixed the inherited colour with blue, and `translateX(var(--missing, initial))` compiled `unset` into the function. Inside a longer value the fallback is now dropped with a warning. An env() fallback is parsed for the declaring property, as a var() fallback is, rather than under the env() name.
A keyword on its own was reported lowercased, while a nested one and every other value warning quote the value as the stylesheet spells it.
A custom property set to inherit, unset, revert or revert-layer was dropped, so an earlier rule's value on the same element won, and one set to initial was dropped with a warning and read the ancestor's value. Each now compiles to a value the runtime resolves where the property is declared: inherit reads the parent's value over every declaration on the element, and initial is the guaranteed-invalid value, which takes a var() fallback and hides an ancestor's declaration (css-variables-1 §2). A declaration a var() leaves without a value is invalid at computed-value time and behaves as unset (css-variables-1 §3.1): color inherits, a runtime-parsed shorthand resets its longhands, and any other property is cleared. A filter list with such an entry has no filter, an empty fallback substitutes nothing, and the inliner never folds an inheriting keyword and compiles a read of an inlined initial the same way. A context lookup reads only a variable's own entries, so var(--constructor) takes its fallback.
…it sits A var() with no fallback for a variable that has no value left its declaration a value when it sat inside a list, a function or another custom property: `--reader: var(--source) 1px` read `1px`, and a custom property whose value failed fell back to its ancestor's declaration instead of the reader's fallback. The lookup now records the failed substitution itself, so the declaration is invalid at computed-value time and a custom property holding one is the guaranteed-invalid value (css-variables-1 §3.1). A channel rncss publishes for a computed value, such as the em, is a default when nothing publishes it, never a failure. The inliner flattens every variable a value reads, inside functions and fallbacks too, before substituting it, so the result no longer depends on declaration order, and leaves each member of a cycle to the runtime. A custom property reading the guaranteed-invalid value is guaranteed-invalid itself, a declaration whose value substitutes to nothing is unset, and an empty custom property keeps its empty value, which substitutes nothing inside a list at runtime as well.
…me too The compiler made a declaration whose value substitutes to nothing unset, while the runtime handed React Native the empty list it left: `margin: var(--a,) var(--b,)` with neither declared rendered `margin: []` over an earlier rule's value. The list resolver records when every entry substituted nothing, which leaves a property's value invalid at computed-value time and a custom property's value empty, as css-variables-1 §3.1 defines.
… left to the runtime
…ation that reads it
…h the compiler resolves
The animation shorthand answered `unset` with nothing, so `animation: initial`, `animation: unset` and `animation: var(--missing)` left an earlier rule's animation running. It now resets every longhand it sets, as the other shorthands do.
…ent's variables are assigned nativewind#474's cascade order over nativewind#391's computed-value-time handling: every rule's variables are assigned before any declaration resolves, and every declaration resolves in cascade order. A declaration a var() leaves invalid resolves to `unset`, and one the runtime cannot read is ignored, so the earlier rule's value stands: `transform: none rotate(10deg)` after `translateX(10px)` keeps the translation. transform, line-height and animation-name now answer `unset` with a reset, since nothing now means ignored.
…or from a var() CSS ignores a filter declaration with a function its grammar rejects, and React Native's processFilter drops the whole list when one entry is invalid, but rncss passed the entry through or dropped it alone: `blur(2px) drop-shadow(2px)` drew the blur, and `brightness(-1)` or `hue-rotate(90)` reached React Native, which then drew no filter at all. A list is now invalid when any entry fails to resolve, so an ignored written filter leaves the earlier rule's value and one from a var() leaves none. Each filter function reads its single argument by the Filter Effects 1 grammar through readers the compiler and the runtime share. The compiler reads every angle through the same reader, so a gradian or a turn reaches React Native in degrees in `hue-rotate()`, `rotate()` and `skew()` alike. A url() entry is still left out with the rest applied, since React Native has no SVG filter to reference.
This was referenced Oct 17, 2026
…ets the property With every declaration resolved in cascade order, a declaration that resolves to nothing is ignored, so a reset whose parser has no answer for `unset` left the earlier rule's value in place. A CSS-wide keyword the compiler hands a runtime parser, and a declaration a var() leaves invalid at computed-value time, now remove the property whatever the parser answers, which the transform, line-height and animation-name parsers no longer need to say each.
Contributor
Author
YevheniiKotyrlo
marked this pull request as ready for review
October 17, 2026 03:15
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
Browsers ignore a
filterdeclaration with a function its grammar rejects, so an earlier rule's filter stands, and avar()holding one leaves no filter. React Native'sprocessFilterdrops the whole list when one entry is invalid. With #391 and #474, which this builds on:blur(2px) drop-shadow(2px)andblur(2px 3px)drawblur(2px).blur(foo) brightness(2),brightness(-1),blur(-1px)andhue-rotate(90)reachprocessFilter, which then draws no filter at all.none blur(2px)clears an earlier rule's filter.drop-shadow()its handler cannot read is kept.drop-shadow(1px 2px 3px 4px red)renders[{}]as written and[{ dropShadow: undefined }]through a variable.filter: hue-rotate(1turn)as an empty entry,[{}], whichprocessFilterthrows on, andtransform: rotate(100grad)as{ rotate: null }, which React Native's transform validation rejects in development.1turnsupplied tohue-rotate()at render reaches React Native as written.Solution
var()that leaves one invalid makes itunset.blur()a non-negative length,hue-rotate()an angle or unitless zero, and every other function a non-negative number or percentage. Several arguments, a keyword among other filters, or adrop-shadow()its handler cannot read leave the list invalid.src/utilities(readNonNegativeLength,readNonNegativeAmount,readAngle), so a written filter and one avar()substitutes take the same decision.readAnglegives a gradian or a turn in degrees, since React Native takes degrees or radians alone, and the compiler reads every angle through it.url()entry is still left out with the rest applied, since React Native has no SVG filter to reference.Tests
src/__tests__/native/filter-list-runtime.test.tsx: each rejected function as written over an earlier rule's filter and through a variable, a keyword among filters, a rejected written filter after a variable one, and a gradian or a turn supplied at render and held in a stylesheet variable. Each runs on the inlined and the kept route.src/__tests__/compiler/compiler.test.tsx: a negative argument or a rejected function inside a list emits no declaration, aurl()entry is left out, and an angle in gradians or turns compiles to degrees without a warning.src/__tests__/native/transform.test.tsx:rotate(),rotate:andskewX()in gradians or turns.src/__tests__/utilities/angle.test.tsandfilter-arguments.test.ts: each reader's accepted and rejected arguments.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, alone and with #500 merged.Device evidence, before and after, the written turn's crash among it: #501 (comment)
Known limits
rotate()orskew()reaches React Native as written, since the runtime's transform functions pass their argument unread.Merge order
This builds on #391 and #474. With both, a declaration a
var()leaves invalid resolves tounsetand one the runtime cannot read is ignored, so an earlier rule's filter stands. Two of the commits here are that combination, and they go once both land: #474's cascade order on top of #391, and the reset it needs, where a CSS-wide keyword or avar()without a value removes the property whatever its parser answers.It shares lines with other open pull requests only through #391's commits, which #391 names, and through
deferred-declarations.test.tsx, which #393 carries from #474.Base
Branched off #391's head, on
main(a5002c5).