Repository navigation
fix(native): narrow a var()-valued font-family to its first family - #417
Open
YevheniiKotyrlo wants to merge 4 commits into
Open
YevheniiKotyrlo wants to merge 4 commits into
YevheniiKotyrlo wants to merge 4 commits into
Conversation
This was referenced Aug 15, 2026
Contributor
Author
React Native's fontFamily is one family name, and an array is a value Fabric refuses. LightningCSS types most font-family declarations and the compiler takes their first family, but a stack it cannot type, such as one with a trailing comma, reached the style as an array, and so did a stack arriving through a var(). narrowFontFamily reduces a stack to its first entry that names a family, reading nested groups in place and skipping an entry that names none, as a browser skips a family it cannot use. The compiler applies it to the unparsed path, leaving a stack led by a var() for render, and applyValue applies it to the value the var() resolves to. A stack with no usable family sets nothing, so a family from an earlier rule stands.
YevheniiKotyrlo
force-pushed
the
rncss/font-family-var-stack
branch
from
October 11, 2026 12:35
74dc986 to
cb5cdf7
Compare
This was referenced Oct 11, 2026
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 21:54
Browsers ignore a written stack with an empty entry, a number, a function or an unquoted reserved ident, and make a stack a variable fills invalid at computed-value time the same way, so the element inherits its family. The narrowing skipped such an entry and took the next family, and it narrowed a stack at compile time even when a later entry was a var(), whose value can still invalidate it. Now any such entry invalidates the stack, a var() anywhere defers it to render, and the compiler reads the written tokens for the empty entries and reserved idents parseUnparsed drops.
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 23:25
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
React Native's
fontFamilyis one family name, and an array is a value Fabric refuses. LightningCSS types mostfont-familydeclarations, and the compiler takes their first family. Two routes still hand React Native the whole stack onmain:var(), which is how a Tailwind theme override reachesfont-sans, resolves to an array at render.Solution
narrowFontFamilyreduces a stack to its first family. A stack is valid only when every entry names a family: browsers ignore a written stack with an empty entry, a number, a function or an unquoted CSS-wide keyword ordefault, and make a stack a variable fills that way invalid at computed-value time. So one such entry invalidates the whole stack, and an invalid stack sets nothing.parseUnparseddrops, and leaves a stack holding avar()anywhere for render, since the variable's value can still invalidate it.applyValueapplies it to the value avar()resolves to.The typed path already takes the first family, which is always a name there, so it is unchanged.
Tests
src/__tests__/compiler/font-family.test.ts: the typed and unparsed paths, each way a written stack is invalid, and avar()stack left for render wherever thevar()sits.src/__tests__/utilities/font-family.test.ts: the reduction itself.src/__tests__/native/font-family-stack.test.ts:applyValue, including the delayed marker it must leave alone.src/__tests__/native/font-family.test.tsx: a resolvedvar()and an entry in it that names no family, avar()behind a family, its fallbacks, an empty fallback inside an entry, quoted names and an inline variable.src/__tests__/vendor/tailwind/typography.test.tsx: the Typography Font Family block, with each theme stack overridden so it reaches render.src/__tests__/utilities/style-descriptor.test.tscoversisStyleFunctionandisStyleDescriptorArray, which fix: resolve react-native-css styling and tooling regressions on Expo 57 #451 fixed and nothing tested.main, 46 of these 170 cases fail. Each of 15 mutations fails its own cases.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: #417 (comment)
Known limits
--f: Helvetica Neue, therefore reads as a stack and narrows toHelvetica. Quoting the name keeps it whole. Telling the two apart needs the variable's separators kept, which affects everyvar()value, so I am raising it separately.var()with no value, and a CSS-wide keyword inside a variable's stack, never reach the reduction: the runtime drops a list entry that resolves to nothing, sovar(--missing), Helveticarenders Helvetica where browsers inherit the family. fix: resolve CSS-wide keywords and the inherited colour as CSS defines them #391 makes such a declaration invalid at computed-value time.font-family: Inter, inheritas two families and unquotes every name, so the typed path cannot tellinheritfrom"inherit"and takesInter; browsers reject the unquoted keyword.Merge order
It shares lines with #389 (
compiler/declarations.ts), #391 (compiler/declarations.ts), #413 (compiler/declarations.ts,utilities/index.ts), #420 (compiler/declarations.ts); whichever lands second rebases.It also shares lines with #491 (
compiler/declarations.ts), #492 (compiler/declarations.ts), #494 (compiler/declarations.ts), #495 (compiler/declarations.ts); whichever lands second rebases.Base
Re-written on
main(a5002c5). #451 fixedisStyleFunctionandisStyleDescriptorArray, which this PR also fixed, so that half is gone and its tests stay as coverage.