Skip to content

fix: emit React Native border-start/end props for border-inline styles - #379

Open
danstepanov wants to merge 1 commit into
mainfrom
fix-logical-border-props
Open

fix: emit React Native border-start/end props for border-inline styles#379
danstepanov wants to merge 1 commit into
mainfrom
fix-logical-border-props

Conversation

@danstepanov

Copy link
Copy Markdown
Member

Closes #378, follow up to nativewind/nativewind#1737.

What

border-inline-start-color, border-inline-end-color, and the width and shorthand variants compiled to props like borderInlineStartColor that React Native does not have, so every border-s-* and border-e-* utility (and border-x-, which Tailwind v4 also emits as border-inline-) was a silent no-op on native.

  • Renames the inline logical border color and width properties to RN's RTL aware border-start-* and border-end-* props, in both the parsed path and the unparsed path that var() based colors take (the original Custom colors on border-s-* and border-e-* don't work nativewind#1737 report).
  • Fixes the border-inline, border-inline-start, and border-inline-end shorthands to emit those props, and fixes border-inline-width which previously only read the start value.
  • Drops per-side border styles since RN only has a uniform borderStyle. solid is dropped silently because it matches the native default; anything else records a compiler warning.
  • Dev builds now print compiler warnings from the metro transformer, so declarations without a native equivalent are visible while building instead of being collected and discarded.

Tests

  • New src/tests/compiler/logical-borders.test.ts covering colors (literal and var()), widths, shorthands, and the style drop and warning behavior.
  • Updated borders.test.tsx expectations, which previously locked in the invalid borderInline* prop names.
  • Full suite, typecheck, and lint pass locally. The three babel suites fail identically on clean main (missing --experimental-vm-modules in this environment) and are unrelated.

border-inline-start-color and friends compiled to props like
borderInlineStartColor that React Native does not support, so every
border-s-* and border-e-* utility was a silent no-op on native. Rename
them to the RTL-aware border-start-* and border-end-* props in both the
parsed and unparsed paths, fix the border-inline shorthands, and drop
per-side border styles (with a compiler warning when the style is not
solid, since solid matches the native default).

Dev builds now print compiler warnings from the metro transformer so
declarations without a native equivalent are visible while building.

Closes #378
@danstepanov danstepanov added the v5 stability Tracking v5 stability for @latest release label Jul 17, 2026
@danstepanov danstepanov added deferred (post-5.0) Not required for the 5.0 latest release; revisit after and removed v5 stability Tracking v5 stability for @latest release labels Jul 17, 2026

@YevheniiKotyrlo YevheniiKotyrlo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming from production (Expo SDK 55 · RN 0.83.6 · Tailwind v4) — border-s-* / border-e-* were silently dropped on native before this. Thanks for fixing it at the parser layer.

I compiled the full logical-border matrix through fix-logical-border-props. Longhands and literal shorthands are handled. One gap remains: var()-valued border-inline shorthands still emit borderInline*.

declaration this branch emits
border-inline-start-color: hsl(var(--p)) — longhand borderStartColor
border-inline-color: red blue — literal shorthand borderStartColor + borderEndColor
border-inline-color: hsl(var(--p))var shorthand borderInlineColor
border-inline-width: var(--w)var shorthand borderInlineWidth

A var() routes the shorthand to parseUnparsedDeclaration, where propertyRename maps only the longhands and parseBorderInline* never runs — so border-inline-color / -width fall through unrenamed. Not hypothetical: Tailwind v4's border-x-<token> emits exactly border-inline-color: var(...) (v4 theme colors are CSS vars), so a themed border-x-primary is still a native no-op after this PR.

propertyRename can't cover it — a shorthand needs a 1→2 expansion, not a 1→1 rename — so the fix goes in the same parseUnparsedDeclaration, mirroring your parseBorderInline*. I've opened it as #393, stacked on this branch so the diff is just the delta (+2 cases, logical-borders at 15/15). Merge it in, cherry-pick the commit, or tell me to retarget — whichever's easiest.

Out of scope by design: the full border-inline: 2px solid var() form (still emits borderInline; needs runtime shorthand splitting, and Tailwind never emits it) — a small follow-up if you want it.

YevheniiKotyrlo added a commit to YevheniiKotyrlo/react-native-css that referenced this pull request Aug 15, 2026
`border-block-*` reaches React Native under keys it has no attribute for, so
the declaration compiles, renders and paints nothing. Measured through this
branch's own compiler and asserted against the props a rendered `View`
receives:

  border-block: 2px solid red   -> borderBlockColor, borderBlockWidth,
                                   borderBlockStyle
  border-block-width: 2px       -> borderBlockWidth
  border-block-start-width: 2px -> borderBlockStartWidth
  border-block-style: dashed    -> borderBlockStyle
  border-block: var(--b)        -> borderBlock: [2, "dashed", "red"]

React Native's support here is not uniform, which is what makes the defect
hard to see. The three block COLOURS are real props — `borderBlockColor`,
`borderBlockStartColor` and `borderBlockEndColor` are in
`ReactNativeStyleAttributes`, in `BaseViewConfig.android.js`, in
`BaseViewConfig.ios.js` and in `ViewStyle`. The block WIDTHS are in
`BaseViewConfig.ios.js` and nowhere else, so an emitted `borderBlockWidth`
paints on iOS Fabric and is dropped on Android and on the old architecture.
No per-edge border STYLE exists at any layer on either platform.

So the colours are kept as they are, the widths map to the physical edges
every platform reads, and the styles drop the way the inline axis already
drops them. `direction` never flips the block axis, so block-start is the top
edge and block-end the bottom one on every platform, which makes the mapping
exact rather than an approximation.

The live trigger is Tailwind: `border-y-1` compiled to
`{ borderBlockWidth: 1, borderBlockStyle: "solid" }`, two keys React Native
ignores, so the utility drew nothing on Android — the block-axis twin of the
`border-x-*` bug nativewind#379 fixed. `src/__tests__/vendor/tailwind/borders.test.tsx`
asserted those two dead keys and passed while broken, exactly as nativewind#378
describes for the inline axis; it now asserts `borderTopWidth` /
`borderBottomWidth`.

Also on the unparsed path: `border-block-color: var(--a) var(--b)` put the
whole two-value list into one key, and `border-block-end-style` was missing
from the parser table so it warned as an unsupported property while
`border-block-start-style` silently emitted a dead key. Both now behave like
their inline-axis twins.

`parseBorderInlineStyle` becomes `parseUnsupportedEdgeStyle` and serves all
six per-edge style longhands: the decision it encodes — which per-edge styles
React Native can express, and how a dropped one is reported — is the same on
both axes, and two copies of it could answer differently.

The two planes are independently load-bearing, by measurement. Removing the
`borderBlock` runtime handler leaves every compiler test green and turns three
native ones red; pointing that handler at `borderBlockWidth` does the same.
A dead key is invisible in the IR, where it looks exactly like a real
declaration, so the assertion has to be made against the props the component
received.

The new sweep is derived rather than restated: it generates all 24
`border-{inline,block}[-start|-end][-width|-style|-color]` properties, drives
each through both the literal and the var() route, and asserts every rendered
key is one React Native declares. The census of real keys carries
`satisfies readonly (keyof ViewStyle)[]`, so a name React Native does not
declare cannot be added to it to make a dead key pass, and a name React Native
drops later turns the type-check red.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deferred (post-5.0) Not required for the 5.0 latest release; revisit after

Projects

None yet

Development

Successfully merging this pull request may close these issues.

border-s-* and border-e-* compile to style props React Native does not support

2 participants