diff --git a/packages/lint/src/rules/gsap.test.ts b/packages/lint/src/rules/gsap.test.ts
index 85089e8eb..e452a20e3 100644
--- a/packages/lint/src/rules/gsap.test.ts
+++ b/packages/lint/src/rules/gsap.test.ts
@@ -1499,6 +1499,22 @@ describe("GSAP rules", () => {
expect(finding).toBeDefined();
});
+ it("gsap_non_transform_motion: fires on a layout/reflow prop that appears only in a fromTo's from-object", async () => {
+ const html = `
+
+
+
+`;
+ const result = await lintHyperframeHtml(html);
+ const finding = result.findings.find((f) => f.code === "gsap_non_transform_motion");
+ expect(finding).toBeDefined();
+ });
+
it("gsap_non_transform_motion: fires on text-reflow props (letterSpacing / fontSize)", async () => {
const html = `
diff --git a/packages/lint/src/rules/gsap.ts b/packages/lint/src/rules/gsap.ts
index baf00a15f..0fa1e8261 100644
--- a/packages/lint/src/rules/gsap.ts
+++ b/packages/lint/src/rules/gsap.ts
@@ -4,6 +4,9 @@ interface LintParsedGsap {
method: string;
position: number | string;
properties: Record;
+ // fromTo() exposes its first ("from") vars object separately; a layout/reflow prop
+ // that appears only here still animates and must be checked.
+ fromProperties?: Record;
duration?: number;
ease?: string;
extras?: Record;
@@ -1198,7 +1201,14 @@ export const gsapRules: LintRule[] = [
...parsed.animations.map((anim) => ({
method: anim.method,
selector: anim.targetSelector,
- properties: Object.keys(anim.properties),
+ // Union the from-vars: a fromTo() can animate a layout/reflow prop that appears
+ // only in its first ("from") object, which is just as stutter-prone as the to-vars.
+ properties: [
+ ...new Set([
+ ...Object.keys(anim.properties),
+ ...Object.keys(anim.fromProperties ?? {}),
+ ]),
+ ],
raw: synthesizeWindowRaw(parsed.timelineVar, anim),
})),
...extractStandaloneGsapTransformCalls(stripJsComments(script.content)),