Skip to content

fix(CSS): leave the second component at zero for single-argument translate and skew - #10385

Open
dennytosp wants to merge 7 commits into
software-mansion:mainfrom
dennytosp:fix/single-argument-translate-and-skew
Open

fix(CSS): leave the second component at zero for single-argument translate and skew#10385
dennytosp wants to merge 7 commits into
software-mansion:mainfrom
dennytosp:fix/single-argument-translate-and-skew

Conversation

@dennytosp

Copy link
Copy Markdown
Contributor

Note

This pull request was authored by AI on behalf of @dennytosp.

Summary

parseTranslate and parseSkew fall back to values[1] ?? values[0], which copies the single argument onto the Y axis:

processTransform('translate(25px)') // -> [{ translateX: 25 }, { translateY: 25 }]
processTransform('skew(30deg)')     // -> [{ skewX: '30deg' }, { skewY: '30deg' }]

CSS Transforms Level 1 says the opposite for both:

  • translate(tx)"If <ty> is not provided, ty has zero value."
  • skew(ax)"If the second parameter is not provided, it has a zero value."

React Native's own parser agrees. Libraries/StyleSheet/processTransform.js pads the list with a literal zero:

if (parsedArgs?.length === 1) {
  parsedArgs.push(0);
}

Confirmed by running it directly against this repo's installed React Native:

RNCORE translate(25px)  ->  [{"translate":[25,0]}]

So transform: 'translate(100px)' moves the element 100px down as well as across in Reanimated, and a CSS transition animating that value drags it diagonally.

Why this reads as a copy-paste slip rather than a design choice

parseScale uses the same values[1] ?? values[0] shape, and there it is correct — scale(s) is uniform per spec. But parseScale also has an explicit values.length === 1 branch above it, so by the time that fallback runs values[1] is always defined and the ?? values[0] is dead. parseTranslate and parseSkew have no such branch, so they inherited a fallback that only made sense for scale.

Fix

Pass 0 as the missing second component. No guard changes were needed: parseTranslateY accepts it as a number, and parseSkewY already maps a zero to '0deg' (values[0] === 0 ? '0deg' : values[0]). Both still emit two entries, so transform lists keep the same length whether or not the second argument was written — which matters for interpolating between two transform strings.

parseScale is deliberately left alone.

Test plan

This changes behavior that three existing assertions in src/common/style/processors/__tests__/transform.test.ts had captured, so those three are updated rather than added to. They fail on main against the spec-correct expectations:

$ git stash push src/common/style/processors/transform.ts
$ yarn jest src/common/style/processors/__tests__/transform.test.ts
  ✕ parses translate(25)     Expected [{translateX:25},{translateY:0}]     Received [{translateX:25},{translateY:25}]
  ✕ parses translate(25%)    Expected [{translateX:'25%'},{translateY:0}]  Received [{translateX:'25%'},{translateY:'25%'}]
  ✕ parses skew(45deg)       Expected [{skewX:'45deg'},{skewY:'0deg'}]     Received [{skewX:'45deg'},{skewY:'45deg'}]
Tests: 3 failed, 61 passed, 64 total

With the fix applied:

$ yarn jest
Test Suites: 103 passed, 103 total
Tests:       1556 passed, 1556 total

Two-argument forms, scale(), translateX/Y(), skewX/Y(), the array input path, and every error case are untouched. yarn eslint and oxfmt --check are clean on both files.

If you would rather keep the current behavior for compatibility reasons, I am happy to close this — but then the two spec references above are worth a comment in the source, because the next reader will file the same report.

Changelog

  • I added an entry to the Unpublished section of each changed package's CHANGELOG.md, or this PR does not change react-native-reanimated or react-native-worklets.

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 9d8b379f-af69-40ee-a44b-dc5767bee94d

📥 Commits

Reviewing files that changed from the base of the PR and between f1819b4 and 023c182.

📒 Files selected for processing (1)
  • packages/react-native-reanimated/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-native-reanimated/CHANGELOG.md

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Updated transform parsing so single-argument translate() and skew() values default the omitted Y-axis argument to zero. Trimmed transform input before parsing. Updated tests for numeric, percentage, angle, and whitespace inputs. Added a changelog entry describing the fix.

Merge Risk: ⚪ Minimal · up to 023c1

Single-argument translate and skew transforms now preserve a zero value for the omitted Y component. No current merge-blocking risk is identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: single-argument CSS translate and skew functions now leave the second component at zero.
Description check ✅ Passed The description accurately explains the existing behavior, the specification-based fix, the affected functions, the tests, and the validation results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…slate and skew

`parseTranslate` and `parseSkew` fell back to `values[1] ?? values[0]`, copying
the single argument onto the Y axis:

    processTransform('translate(25px)') -> [{ translateX: 25 }, { translateY: 25 }]
    processTransform('skew(30deg)')     -> [{ skewX: '30deg' }, { skewY: '30deg' }]

CSS Transforms Level 1 says the opposite for both: for `translate(tx)`, "if
<ty> is not provided, ty has zero value", and for `skew(ax)`, "if the second
parameter is not provided, it has a zero value". React Native's own parser
agrees — `processTransform` pads the list with a literal zero:

    if (parsedArgs?.length === 1) {
      parsedArgs.push(0);
    }

so `translate(25px)` gives `[{ translate: [25, 0] }]` there.

The fallback looks like it was carried over from `parseScale`, where repeating
the argument is correct because `scale(s)` is uniform. It is not correct here:
`transform: 'translate(100px)'` moved the element 100px down as well as across.

Pass 0 instead. `parseTranslateY` accepts it as a number and `parseSkewY`
already maps a zero to `'0deg'`, so both keep emitting two entries and transform
lists still line up for interpolation. `parseScale` is left as it is.
@dennytosp
dennytosp force-pushed the fix/single-argument-translate-and-skew branch from f3561df to 76e90fc Compare August 26, 2026 14:25
@MatiPl01 MatiPl01 self-assigned this Aug 26, 2026
MatiPl01 added a commit that referenced this pull request Aug 26, 2026
> [!NOTE]
> This pull request was authored by AI on behalf of @dennytosp.

## Summary

`processTransform` splits the string on `/\)\s*/`, so whitespace
**after** each `)` is consumed, and `filter(Boolean)` drops the empty
part a trailing one leaves behind. Nothing ever reaches the start of the
string, so the first function keeps its padding:

```ts
processTransform('  translate(25px) rotate(45deg)')
// throws: [Reanimated] Invalid transform property:   translate(25px)
```

`parseTransformProperty` splits that part on `/\(\s*/`, gets `'
translate'` as the key, matches no `case`, returns `[]`, and
`processTransform` turns the empty result into a thrown error. Only the
first function in the string is affected — everything after a `)` is
already fine.

The same string without the two leading spaces parses without complaint,
and React Native's parser accepts both:

```
RNCORE '  translate(25px) rotate(45deg)'  ->  [{"translate":[25,0]},{"rotate":"45deg"}]
```

CSS ignores leading whitespace in a property value, and a value
assembled from a template literal or read out of a config file easily
carries some — this is the kind of difference that produces a crash
report with no obvious cause.

### Fix

`value.trim()` before the split. Trimming the whole input rather than
the parsed key also covers tabs and newlines, and leaves the error
message intact for genuinely invalid input.

## Test plan

Two cases added to the multi-transform table in
`src/common/style/processors/__tests__/transform.test.ts` — one with
leading and trailing spaces, one with newlines and indentation. Both
**fail on `main`**:

```console
$ git stash push src/common/style/processors/transform.ts
$ yarn jest src/common/style/processors/__tests__/transform.test.ts
  ✕ parses   translate(25, 25) scale(2)
  ✕ parses \n  translate(25, 25)\n  scale(2)\n
Tests: 2 failed, 64 passed, 66 total
```

With the fix applied:

```console
$ yarn jest
Test Suites: 103 passed, 103 total
Tests:       1558 passed, 1558 total
```

Every existing error case still throws — `translat(25, 25)`,
`rotate(90)`, `scaleX()` and the rest are unaffected, since trimming
only removes padding that was never part of a function name. `yarn
eslint` and `oxfmt --check` are clean on both files.

Touches the same file as #10385 but a different function; the two apply
independently.

## Changelog

- [x] I added an entry to the `Unpublished` section of each changed
package's `CHANGELOG.md`, or this PR does not change
`react-native-reanimated` or `react-native-worklets`.

---------

Co-authored-by: Mateusz Łopaciński <lop.mateusz.2001@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants