The difference between offsetTop, scrollTop & clientTop (& why you should never use them)

Arnav ZeK
3 min readJul 20, 2020

--

The cursor position is accessed by clientX and clientY from the event argument, this is not dependent on scroll

At the top left corner, it is 0,0 and on the bottom right corner it is max, max

The position of the element can be accessed in terms of the scroll by offsetTop and offsetLeft, offset signifies that it respects padding and border width

clientTop and clientLeft don’t respect border-width but they do respect padding

Evidently offsetTop and offsetLeft are read-only property

scrollTop is the vertical scroll distance inside that element, scrollLeft is the same for horizontal scroll

pageXOffset is the horizontal scroll distance

pageYOffset is the vertical scroll distance

offsetTop is read-only, while scrollTop is read/write.

clientWidth = width + padding

clientHeight = height + padding

offsetWidth = width + padding + border

offsetHeight = height + padding + border

Let’s implement a drag system with the gained knowledge

As event.clientX and event.ClientY values are not dependent on scroll and element.offsetTop and offsetLeft are dependent on the scroll. It is required to convert the cursor position to scroll dependent variable or offset position to scroll independently.

Let’s try the latter

element.style.top = event.clientY -(parent.offsetTop- pageYOffset )element.style.left= event.clientX -(parent.offsetLeft- pageXOffset )

percentage positioning

element.style.top = (((event.clientY -(parent.offsetTop -pageYOffset ) )/parent.offsetHeight) * 100)+'%'element.style.left = you get the idea

The Alternative

offsetTop has a bug, making it’s value suddenly become 0, same with offset left

element.getBoundingClientRect provides a better alternative because it’s free from bugs (duh!) & it is scroll independent so less code

let rect = parent.getBoundingClientRect();element.style.top = event.clientY - rect.top + 'px'element.style.left = event.clientX - rect.left +'px'

percentage positioning

let rect = parent.getBoundingClientRect();element.style.top=((event.clientY - rect.top)/parent.offsetHeight) * 100+'%'element.style.left=((event.clientX-rect.left)/parent.offsetWidth) *100+'%'

One more Problem

The element being dragged will snap to the cursor position, it will look rough

We must keep the offset of the position cursor, but how?

cursor position relative to parent - position of element relative to the parentsubtract this offset position from new element positionON MOUSE DOWN ---------------------------------------let parentRect = event.target.parentElement.getBoundingClientRect();let childRect = event.target.getBoundingClientRect();
offsetX = (event.clientX - parentRect.left) - (childRect.left - parentRect.left)offsetY = (event.clientY - parentRect.top) - (childRect.top - parentRect.top)ON MOUSE MOVE ---------------------------------------
let parent = event.target.parentElement
let rect = parent.getBoundingClientRect();element.style.top = ((((event.clientY - rect.top)-this.offsetY )/parent.offsetHeight) * 100)+' % 'element.style.left = ((((event.clientX - rect.left ) -this.offsetX)/parent.offsetWidth) *100)+' % '

👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋

Not yet, there is still one more problem. If element is scaled or any other transform is applied, the abrupt snap will come hunting

Solution

use change in the cursor position, it will also decrease code as we on longer to calculate offset between cursor and element

class draggable{constructor(element){
this.element = element
element.addEventListener('click',this.mouseClicked.bind(this))
element.addEventListener('mousemove',this.mouseMoving.bind(this))
element.addEventListener('mouseup',this.mouseRemoved.bind(this))
}setPreviousCursorPosition(event){
this.previousCursorPosition.x = event.clientX
this.previousCursorPosition.y = event.clientY
}
mouseClicked(event){
this.setPreviousCursorPosition(event)
this.mouseDown = true
this.selectedOverlayIndex = event.target.dataset.index
}
mouseMoving(event){if(!this.mouseDown) return
let parent = event.target.parentElement
function toNumber(val){
return Number(val.replace('px'))
}let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.y
this.element.style.top = (toNum(element.style.top) + dy) + 'px'
this.element.style.left = (toNum(element.style.left) + dx) + 'px'
}mouseRemoved(){ this.mouseDown = false}}

But the position of an element would not adapt if the container size changes. so we need to change px to %

solution

  • We need to convert percentage value to pixels
y: parent.offsetWidth*(percentageX/100)x: parent.offsetHeight*(percentageY/100)
  • Add dy and dx to the new position we got from above
  • after adding dy and dx to corresponding values convert them to percentage with respect to the element’s parent height and width
y: ( afterAddingdy/parent.offsetHeight) * 100x: ( afterAddingdx/parent.offsetWidth) * 100

Whole & Final Code

class draggable{constructor(element){
this.element = element
element.addEventListener('click',this.mouseClicked.bind(this))
element.addEventListener('mousemove',this.mouseMoving.bind(this))
element.addEventListener('mouseup',this.mouseRemoved.bind(this))
}setPreviousCursorPosition(event){
this.previousCursorPosition.x = event.clientX
this.previousCursorPosition.y = event.clientY
}
mouseClicked(event){
this.setPreviousCursorPosition(event)
this.mouseDown = true
this.selectedOverlayIndex = event.target.dataset.index
}
mouseMoving(event){if(!this.mouseDown) return
let parent = event.target.parentElement
function toNumber(val){
return Number(val.replace('%'))
}
let dx = event.clientX - this.previousCursorPosition.x
let dy = event.clientY - this.previousCursorPosition.y
function percentageToPixel(x,y){
return {
x:parent.offsetWidth*(x/100),
y:parent.offsetHeight*(y/100)
}
}
function pixelToPercentage(x,y){
return {
y:( y /parent.offsetHeight) * 100,
x:( x /parent.offsetWidth) * 100
}
}
let pixelPosition = percentageToPixel(toNumber(element.style.left, toNumber(element.style.top))let top = pixelPosition.y + dy
let left = pixelPosition.x + dx
let percentagePosition = pixelToPercentage(left,top)this.element.style.top = percentagePosition.y+'%'
this.element.style.left = percentagePosition.x+'%'
}mouseRemoved(){
this.mouseDown = false
}
}

More resources

https://www.programmersought.com/article/5562254337/

--

--