Fix useScroll target not accounting for CSS translate#3655
Fix useScroll target not accounting for CSS translate#3655mattgperry wants to merge 1 commit intomainfrom
Conversation
calcInset() used offsetLeft/offsetTop to calculate the target element's position relative to the scroll container. These properties reflect layout position only and ignore CSS transforms (translate, transform: translateY, etc.), causing useScroll to report incorrect scroll progress when the target has a CSS translation. Added addTranslateOffset() which reads the element's computed translate and transform styles, extracting the translation component from each. This offset is added alongside offsetLeft/offsetTop for every element in the offsetParent chain. Fixes #2914 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes Changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["calcInset(element, container)"] --> B["Walk offsetParent chain"]
B --> C{"isHTMLElement?"}
C -- yes --> D["inset.x += offsetLeft\ninset.y += offsetTop"]
D --> E["addTranslateOffset(inset, current)"]
E --> F{"CSS translate\n!= 'none'?"}
F -- yes --> G["Parse 'translate' parts\nAdd x/y (px only, no %resolve)"]
F -- no --> H{"CSS transform\n!= 'none'?"}
G --> H
H -- yes --> I["Regex: match matrix(...)"]
I --> J{"matrix() matched?"}
J -- yes --> K["values[4] → inset.x\nvalues[5] → inset.y"]
J -- no --> L["⚠️ matrix3d(...) NOT handled\nTranslation silently ignored"]
K --> M["current = offsetParent"]
L --> M
H -- no --> M
M --> B
C -- SVG --> N["getBoundingClientRect diff"]
N --> M
C -- break --> O["Return inset"]
M --> O
Last reviewed commit: 70a1b17 |
| if (transform && transform !== "none") { | ||
| const match = transform.match(/matrix\(([^)]+)\)/) | ||
| if (match) { | ||
| const values = match[1].split(",") | ||
| inset.x += parseFloat(values[4]) | ||
| inset.y += parseFloat(values[5]) | ||
| } | ||
| } |
There was a problem hiding this comment.
matrix3d transforms not handled
The regex only matches the 2D matrix(a, b, c, d, tx, ty) form. Whenever an element has a 3D CSS transform — e.g. translate3d(100px, 200px, 0), translateZ(10px), or perspective(...) — the browser resolves getComputedStyle(el).transform to matrix3d(...), not matrix(...). The current regex won't match, so the X/Y translation components are silently dropped and the scroll progress will still be wrong for those cases.
For matrix3d, the tx/ty values are at index 12 and 13 (0-indexed):
matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, tx,ty,tz,1)
Suggested fix:
if (transform && transform !== "none") {
const matrix3dMatch = transform.match(/matrix3d\(([^)]+)\)/)
if (matrix3dMatch) {
const values = matrix3dMatch[1].split(",")
inset.x += parseFloat(values[12])
inset.y += parseFloat(values[13])
} else {
const match = transform.match(/matrix\(([^)]+)\)/)
if (match) {
const values = match[1].split(",")
inset.x += parseFloat(values[4])
inset.y += parseFloat(values[5])
}
}
}| if (translate && translate !== "none") { | ||
| const parts = translate.split(" ") | ||
| inset.x += parseFloat(parts[0]) || 0 | ||
| inset.y += parseFloat(parts[1] || "0") || 0 | ||
| } |
There was a problem hiding this comment.
Percentage values in
translate property not resolved
getComputedStyle(el).translate returns the computed value, which preserves <percentage> values (e.g. "50% 100px" is returned as-is, not resolved to pixels). parseFloat("50%") returns 50, treating it as 50 px regardless of the element's actual size.
This only affects the standalone CSS translate property (not transform: translateX(...), which does resolve to a pixel matrix). If users write translate: 50% 0, the inset will be wrong.
Consider resolving the percentage against the element's bounding box, or documenting the limitation:
function resolveTranslateValue(value: string, elementSize: number): number {
if (value.endsWith("%")) {
return (parseFloat(value) / 100) * elementSize
}
return parseFloat(value) || 0
}
// then in addTranslateOffset:
if (translate && translate !== "none") {
const parts = translate.split(" ")
const { offsetWidth, offsetHeight } = element
inset.x += resolveTranslateValue(parts[0], offsetWidth)
inset.y += resolveTranslateValue(parts[1] ?? "0", offsetHeight)
}
Summary
useScrollwith atargetelement now correctly accounts for CSStranslateandtransformtranslations when calculating scroll progresscalcInset()used onlyoffsetLeft/offsetTopwhich ignore CSS transforms, causing incorrect scroll progress when the target has a CSS translationaddTranslateOffset()helper that reads the element's computedtranslateandtransformstyles, extracting translation offsets from the matrixHow it works
For each element in the
offsetParentchain, we now also read:translateproperty (e.g.,translate: 100px 200px)transformproperty's resolved matrix (e.g.,matrix(1, 0, 0, 1, 0, 500)fromtransform: translateY(500px))The translation components are added to the layout-based
offsetTop/offsetLeftvalues, giving the correct visual position.Test plan
scroll-target-translate) verifies scroll progress accounts fortransform: translateY(500px)on targetscroll-target-transformtest still passesFixes #2914
🤖 Generated with Claude Code