feat(lint): add gsap_css_transform_conflict - #106
Conversation
f798ace to
ced72b7
Compare
vanceingalls
left a comment
There was a problem hiding this comment.
Good rule — this is a real footgun in GSAP compositions and worth catching at lint time. A few issues to address before merging:
1. The fromTo exemption doesn't actually exist
The PR description and fix hint both say the safe pattern is tl.fromTo(...) with explicit xPercent — but the rule code never checks which tween method was used. GsapWindow doesn't have a method field (extractGsapWindows drops it), so tl.fromTo("#title", { xPercent: -50 }, { xPercent: -50, x: 0 }) on an element with transform: translateX(-50%) in CSS will still fire because x and xPercent are in the properties list and the CSS selector matches.
The negative test ("does NOT report error when tl.to animates x on element WITHOUT CSS transform") passes for the wrong reason — it passes because #card has no CSS transform, not because it uses fromTo. Rename the test or add the actual fromTo-on-CSS-transform case.
Two options:
- (a) Add a
methodfield toGsapWindowso the rule can skipfromTocalls (if the intent is "fromTo means the author is managing transforms explicitly"). - (b) Keep firing on
fromTotoo — but then update the fix hint, since it currently recommends something the rule would also flag.
I'd lean toward (a). The whole point of fromTo is that the author sets both ends explicitly, so GSAP overwriting CSS is intentional.
2. Negative test is misleadingly named
it("does NOT report error when tl.to animates x on element WITHOUT CSS transform")
The test body uses tl.fromTo, not tl.to. And it tests absence of CSS transform rather than the fromTo exemption. Either fix the name to match the body, or split into two tests:
fromToon an element WITH CSS transform → no finding (once the exemption exists)tl.toon an element WITHOUT CSS transform → no finding
3. Inline style transforms are invisible to this rule
The rule only scans <style> blocks. An element like:
<div id="title" style="transform: translateX(-50%)"></div>with no corresponding <style> rule would silently pass. This is common in AI-generated compositions. Not blocking, but worth a follow-up or a comment in the code acknowledging the gap.
4. CSS selector regex only handles bare #id / .class
The regex ([#.][a-zA-Z0-9_-]+)\s*\{([^}]+)\} won't match compound selectors like #root .title, grouped selectors like #a, #b, or attribute selectors. Compositions typically use flat ID selectors so this is low risk — but add a code comment noting the limitation so future maintainers don't assume full CSS parsing.
5. Dual findings for combined transforms
An element with transform: translateX(-50%) scale(0.8) will appear in both cssTranslateSelectors and cssScaleSelectors. If the tween animates both x and scale, two separate findings fire for the same element. Consider deduplicating into a single finding listing all conflicting properties.
05dd365 to
c4ce52e
Compare
|
All 5 points addressed — force-pushed to the same commit. 1. fromTo exemption is now real 2. Tests renamed and split
3. Inline style gap documented 4. CSS selector regex limitation documented 5. Deduplication |
Detects elements whose CSS <style> block sets `transform: translate*` or `transform: scale*` that are also targeted by a GSAP tl.to/tl.from tween animating x, y, xPercent, yPercent, or scale. GSAP's transform properties overwrite the *entire* CSS transform, silently discarding translateX(-50%) centering and similar positioning tricks. tl.fromTo is exempt: when the author provides explicit from/to states they own both ends of the transform, so overwriting CSS is intentional. Combined transforms (translateX(-50%) scale(0.8)) that conflict with multiple tween properties produce a single deduplicated finding. Adds a method field to GsapWindow so the rule can distinguish tl.to/from (conflict) from tl.fromTo (exempt). Known limitations noted in comments: inline style transforms are not detected; CSS selector regex handles bare #id/.class only. Tests: tl.to on CSS translateX → warn; tl.to on CSS scale → warn; tl.fromTo on CSS translateX → no finding (exempt); tl.to without CSS transform → no finding; combined transform → single finding. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
c4ce52e to
59dddd9
Compare
## Changes
Added a new lint rule `gsap_css_transform_conflict` that detects when GSAP animations will silently overwrite CSS transforms.
**`gsap_css_transform_conflict` (error)** — fires when an element has `transform: translateX(-50%)` or `transform: scale()` in CSS and a GSAP `tl.to/from` tween animates `x`, `y`, `xPercent`, `yPercent`, or `scale`. GSAP silently overwrites the full CSS transform, discarding centering tricks like `translateX(-50%)`. Fix hint guides authors to the safe `fromTo` + `xPercent` pattern.
## Root cause
This bug surfaced while building compositions where title reveals were placed off-center because `tl.to("#title", { x: 0 })` stripped the `translateX(-50%)` centering from CSS.
## Test coverage
- [x] `gsap_css_transform_conflict` — `tl.to` with `x` on CSS `translateX` element → error
- [x] `gsap_css_transform_conflict` — `tl.to` with `scale` on CSS `scale()` element → error
- [x] `gsap_css_transform_conflict` — `tl.fromTo` without CSS transform → no finding
Changes
Added a new lint rule
gsap_css_transform_conflictthat detects when GSAP animations will silently overwrite CSS transforms.gsap_css_transform_conflict(error) — fires when an element hastransform: translateX(-50%)ortransform: scale()in CSS and a GSAPtl.to/fromtween animatesx,y,xPercent,yPercent, orscale. GSAP silently overwrites the full CSS transform, discarding centering tricks liketranslateX(-50%). Fix hint guides authors to the safefromTo+xPercentpattern.Root cause
This bug surfaced while building compositions where title reveals were placed off-center because
tl.to("#title", { x: 0 })stripped thetranslateX(-50%)centering from CSS.Test coverage
gsap_css_transform_conflict—tl.towithxon CSStranslateXelement → errorgsap_css_transform_conflict—tl.towithscaleon CSSscale()element → errorgsap_css_transform_conflict—tl.fromTowithout CSS transform → no finding