Position/Dimension properties in Javascript

Position/Dimension properties in Javascript

Fundamental concepts

  • In general, x/ left suffices refer to the horizontal distance, while y/ top suffices refer to the vertical one.
  • Viewport is the area/portion of the web page (precisely speaking, the nearest iframe) that we can see on the screen. Imagine a web page is an infinite landscape, your web browser is a camera and the area which is seen through the camera viewfinder is the viewport. When you move the camera/viewfinder, the scene changes respectively.
  • Floating-point values can be returned by or assigned to some properties, especially when the browser is zoomed or when elements are transformed. We will see in this post that scrollTop, scrollLeft, DOMRect are able to handle floating-point, while the others are rounded to integer numbers.
  • getBoundingClientRect() and getClientRects() return the rendered dimension which takes care of transforms while the others APIs return the layout dimension and ignore all transforms.
  • The order of an element's displaying components from inner to outer is as follows: content → padding → scroll → border → margin. Verbally speaking, the area from content to the border is considered to belong to the element (is involved in the results returned from getBoundingClientRect or in offset* properties). Of which, only content and padding belong to the client area of an element (involved in client* properties).
  • The vertical scroll bar does not always stay on the right side of an element. For example, in a right-to-left mode, it is rendered on the left side.
  • While this is not related to any javascript API (except the window.getComputedStyle()), it is worth noting. box-sizing CSS property defines whether the CSS width/height includes padding, border (and thus, scroll bar).
    In the CSS standard, by default, box-sizing is content-box, implying that the CSS width/height properties define only the dimension of the content.
    While in practice, many frameworks, modern CSS reset library assign border-box to box-sizing, which means padding, border, scrollbar are included in the CSS width/height.
    In bootstrap's _reboot.scss:
// Document
//
// Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.

*,
*::before,
*::after {
  box-sizing: border-box;
}
  • When an element becomes a scroll view (usually due to the larger size of its children), its content is the viewable area, not the larger being scrolled area which is the content of its children.

DOM node hierarchy inheritance structure

The most generic class is the node class, its unique superclass is EventTarget. There are many types of nodes. The nodeType property is an integer used to define the type of a node, hence, the sub-class to which it belongs. The list of node types sorted by their popularity are:

  • Node.ELEMENT_NODE: infers the sub-class Element.

Element is the most common node type with its 2 common descendant classes are HTMLElement ( <a>, <p>, <iframe> ... elements), and SVGElement ( <svg>, <path>, <g> ... elements).

  • Node.TEXT_NODE: infers the sub-class Text. For example: <span>hello world</span> is a Element node with a single Text node whose wholeText property is 'hello world'.
  • Node.COMMENT_NODE: infers the sub-class Comment, represents comment nodes <!-- ... -->.
  • Node.DOCUMENT_NODE: infers the sub-class Document where the document variable instantiated from.
  • Node.DOCUMENT_TYPE_NODE: infers the sub-class DocumentType, represents <!DOCTYPE html> node.
  • Node.CDATA_SECTION_NODE: infers the sub-class CDATASection, represents <!CDATA[[ ... ]]> nodes.
  • Other node types are less common: Node.ATTRIBUTE_NODE (Attribute sub-class), Node.PROCESSING_INSTRUCTION_NODE (ProcessingInstruction sub-class), Node.DOCUMENT_FRAGMENT_NODE (DocumentFragment sub-class).
  • And some types are deprecated: Node.ENTITY_REFERENCE_NODE, Node.ENTITY_NODE, Node.NOTATION_NODE.
window instanceof Window // true
window instanceof Node // false

document instanceof Node // true
document instanceof Document // true
document instanceof Element // false

document.documentElement instanceof Node // true
document.documentElement instanceof Element // true
document.documentElement instanceof HTMLElement // true

document.getElementsByTagName('path')[0] instanceof HTMLElement // false
document.getElementsByTagName('path')[0] instanceof SVGElement // true

Suffix naming convention

Common (not always true, for example, mouse event's client* are exceptions), suffices used in properties are:

  • screen*: refer to the position in the whole Operating System. If there are multiple monitors, the monitor in left and top are also involved.
  • page*: related to the web page root element, document.documentElement.
  • scroll*: when an element turns into a scroll view, this refers to the whole area which requires the scroll.
  • offset*: element's rendered area (from inner up to border).
  • client*: element's area used to display its children (from inner up to padding).

Root element is a special case

The root element (the <html> element in quirks mode, or, the <body> element when not in quirks mode) is an instance of HTMLElement (thus, Element). It has the same properties as all other HTML elements, but some of the properties have particularly different definitions by the specs. By convention, through this post, I will use the term root-element, and it should be considered as an <html> or <body> element depending on whether the document is in quirks mode.

The following codepen explains why the root-element should be treated differently. border, padding styling are normally applicable but overflow has no effect. The children always overflow the root element regardless of the overflow styling. The height styling determines the position of the border but the background is applied on the whole web page instead of the area within the border.

To rephrase it, the scroll bar will never appear on the root element. The specs take this advantage to control the browser's scroll bar (window.scroll*) when the APIs are called on the root document (document.documentElement.{scroll*,client*}).

!!Important note!!: the browser's scroll bar does NOT belong to the root element, check the codepen, when the scroll bar appears, it stays outside of the root element's border.

Mouse event

Mouse Event's properties, in a mouse-related events, such as mousedown, mouseenter, mouseleave, mousemove, mouseout, mouseover.

  • clientX, clientY.
    Mouse position relative to the viewport  (of the nearest iframe). Try running the code pen. Note that the code pen itself is an embedded iframe.

    Compatibility is unknown in Firefox, IE, Safari.
  • movementX, movementY.
    Mouse position relative to the previous mouse event.

    Not supported in IE.
  • pageX, pageY.
    Mouse position relative to the whole web page content (of the nearest iframe).

    Not supported in Firefox. Compatibility is unknown in IE, Safari.
  • screenX, screenY.
    Mouse position relative to your operating system's displaying screen. Note that if you have multiple monitors, left and top monitors' sizes are added to these values.

    Compatibility is unknown in Firefox, IE, Safari.

These values are all floating-point numbers.

Codepen

Compatibility

While all properties' compatibility is unknown in Firefox, Safari, the unknown compatibility is likely considered as supported rather than unsupported. For pageX, pageY which are not supported in Firefox, you can use this polyfill from jquery.

			// Calculate pageX/Y if missing and clientX/Y available
			if ( event.pageX == null && original.clientX != null ) {
				eventDoc = event.target.ownerDocument || document;
				doc = eventDoc.documentElement;
				body = eventDoc.body;

				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
			}

None-standard properties

Element

The codepen for this section.

getClientRects() typically returns an array of a single DOMRect object. Except, for <textarea>, <svg>, display: none elements or elements that are not directly rendered, the returned list is empty.
For <span> elements rendered in multiple lines, multiple DOMRect objects are included in the returned array.

In general, getBoundingClientRect is adequate for general uses.

These methods take care of transforms, while other properties ( client*, scroll*, offset*, screen*, inner*, outer*, ...) return the layout dimension which does not involve transforms. From MDN Web Docs:

In case of transforms, the offsetWidth and offsetHeight returns the element's layout width and height, while getBoundingClientRect() returns the rendering width and height. As an example, if the element has width: 100px; and transform: scale(0.5); the getBoundingClientRect() will return 50 as the width, while offsetWidth will return 100.

Both these 2 APIs have high compatibility and are very safe to be used. However, the returned DOMRect objects require more careful treatment. The specs and caveats for the DOMRect object are worth a separate sub-section later in this section.

When the element is the root-element, and the browser's scroll bar appears, getBoundingClientRect() excludes the scroll bar area because the scroll bar does not belong to the root element. There is not a special definition here.

Similar to clientWidth / clientHeight, in the special case, window becomes the scrolling element itself. Note that although having the same names, these methods are different from window.scroll(), window.scrollBy(), window.scrollTo(). In IE, all these methods are not available (this is not true for window's methods).

Safari supports all these APIs but does not support the scroll ScrollToOptions option, which specifies the behavior option for the smooth scroll. However, a polyfill is available. If you are using ES6 dynamic import:

if (!('scrollBehavior' in document.documentElement.style)) {
//safari does not support smooth scroll
  (async () => {
    const {default: smoothScroll} = await import(
      /* webpackChunkName: 'polyfill-modern' */
      'smoothscroll-polyfill'
      )
    smoothScroll.polyfill()
  })()
}

This API is supported by all major browsers. However, IE and Safari do not support the scrollIntoViewOptions, which specifies behavior, block, inline, and the being proposed scroll (or scrollMode?, it is going to fixed) options.

There is a popular ponyfill (in short, a polyfill that does not hook into the native implementation and ignore the native implementation if exists, which eliminates the inconsistency across browsers due to bugs, etc.).  To support the if-needed scrollMode use the scroll-into-view-if-needed package (and optionally use the smooth behavior if the browser natively supports it), to support both the smooth behavior and if-needed mode, use the smooth-scroll-into-view-if-needed. Note that the latter does not follow the specs and adds additional features on its own. Both these 2 packages use the popular package compute-scroll-into-view internally to positioning the element.

  • clientHeight, clientWidth.
    The values are rounded to integer numbers. They are read-only. If the element is inline or has no associated CSS layout box, these values become 0.

    They are equal to content size + padding size (excluded if hidden in a scroll view), the scroll bar size and outer components are not included.

    There is a special case of this definition if the element is the root-element, these values are equal to the view port's size excluding scroll bar size (if present). The special treatment is logically acceptable because the root element when being larger than the viewport, requires a parent element (with a scroll bar) to scroll around, but it is already the root element, so it becomes the scroll view of itself.
Image from MDN Web Docs
  • clientLeft, clientTop.
    The values are rounded to integer numbers. They are read-only. If the element is inline or has no associated CSS layout box, these values become 0.

    These values have corresponding meanings with clientHeight/ clientWidth, which are the width of the top/left border. In the case of clientLeft, if the scroll bar appears on the left side, the scroll bar width is included.

    Because the root-element can have a border, there is not any special definition for the root element. Because the root element can not obtain a scroll bar, these values are always equal to the border dimension.
  • scrollHeight, scrollWidth.
    The values are rounded to integer numbers. They are read-only.

    In conjunction with clientHeight/ clientWidth, these values represent the size of the area scrolled by the element's scroll. That is to say, the dimension required to include all element's children, including the pseudo-elements such as ::before/ ::after. When the children fit into the element size, these values are equal to clientHeight / clientWidth, respectively.
Image from MDN Web Docs
  • scrollLeft, scrollTop.
    These values can be modified to move the scroll position. They can receive/keep decimal values.
    scrollLeft is non-negative in the left-to-right setting, non-positive in the right-to-left setting, while scrollTop is always non-negative.
    In value assignment, the values are clipped in the acceptable range (defined by the scrollable value) before the assignment.
    When there is no scroll bar, these values are 0.

    There is a special definition if the element is the root-document, these values refer to window.scrollX / window.scrollY, respectively, in both value retrieval and assignment.

When the scroll reaches the end, these expressions will be evaluated  true (depend on the scrolling axis). They can be used to determine whether the user scrolls to the end of an element.

// horizontal scroll
elm.scrollWidth === Math.abs(elm.scrollLeft) + elm.clientWidth

// vertical scroll
elm.scrollHeight === elm.scrollTop + elm.clientHeight

The DOMRect object

DOMRect class inherits DOMRectReadOnly (IE, Edge). They have the same properties except that DOMRectReadOnly does not allow modification to the object.

The available properties in this object are left, top, right, bottom, x, y, width, height. They describe the rendered dimension/position of the element relative to the top-left corner of the viewport (not the document). For large elements that are covered by a scroll view, the whole element (including the hidden area) is considered. Content, padding, border (but not margin) are involved in the value of these properties.
To produce a high compatibility code, you should rely only on four properties left, top, right, bottom and calculate other properties manually if needed.

Because properties in DOMRect are not own properties. In other words, they do not appear in Object.keys(), and only appear in for ... in operator. So, the ES6 rest/spread operator or Object.assign() are not able to copy the properties.

None-standard properties

HTMLElement

HTMLElement inherits the Element class. Hence, it has all properties of Element, plus the following properties:

  • offsetHeight, offsetWidth.
    The values are rounded to integer numbers. They are read-only.

    These values have the same meaning with height/ width returned in Element.getBoundingClientRect() except that offset* ignores transforms.
  • offsetParent.
    Returns the reference to the parent element in the layout hierarchy.

    This value can be null in cases:
    + The element does not have an associated CSS layout box (it or its parents have a none display).
    + The element is the root element ( <html>) or <body> element.
    + The element has a fixed position. Note: Firefox returns the <body> element.
  • offsetLeft, offsetTop.
    Distances from the outer border of the element to the inner border of its offsetParent element.

ElementCSSInlineStyle

  • style.
    The returned object can be modified and the change is reflected to the inline style of the element.

Window

  • scrollX, scrollY.
    Special case of Element.scrollLeft / Element.scrollTop.

    In IE, this value is rounded to an integer number.
  • pageXOffset, pageYOffset.
    Alias of Window.scroll* but are read-only.
  • innerHeight, innerWidth.
    read-only values, which hold the layout dimension of the viewport, including the scroll bar area (if exist). They can be changed using resizeBy()/ resizeTo().

    Note that in the mobile devices, when users use pinching gestures to zoom in/out, these values do not change, because the rendered document does not change.
  • outerHeight, outerWidth.
    read-only values which store the dimension of the browser window. They can be changed using resizeBy()/ resizeTo().
From MDN Web Docs
  • screenLeft = screenX, screenTop = screenY.
    read-only values which present the position of the viewport related to the screen (multiple monitors are included). They can be changed using moveBy()/ moveTo().

screenX/ screenY are defined in W3 specs, while screenLeft / screenTop are implemented due to their popular usage. They are all well-supported by all major browsers.

Try this example from MDN, open it, and move your browser window. The rendered circle position keeps the same when you move the browser window around.

  • resizeBy(), resizeTo().
    Used to resize the window. However, due to security reasons, this API is going to be restricted. Ones should wrap the call in a try ... catch block and prepare a fallback operation in the case of failure.

    At present, Firefox requires the window created by window.open() with the 'resizable' feature, containing exactly one tab, and having the same origin as the opener window. Sample code from MDN web docs:
// Create resizable window
myExternalWindow = window.open("http://myurl.domain", "myWindowName", "resizable");

// Resize window to 500x500
myExternalWindow.resizeTo(500, 500);

// Make window relatively smaller to 400x400
myExternalWindow.resizeBy(-100, -100);
  • scroll(), scrollBy(), scrollTo().
    The associated methods of Element class in special cases.
    These methods have much higher compatibility with IE than the ones in the Element class. They are all available in IE, except that the scrollBy() method only supports scrollBy(xCord, yCord) interface.
    The compatibility does not change for other major browsers.
  • moveBy(), moveTo().
    Used to move the window position. At the moment, Firefox requires
  • getComputedStyle().
    The returned object is read-only.

None-standard properties

Screen

  • availTop / availLeft.
    These are not standard properties but implemented in all major browsers except IE.

    Used to determine the total dimension of available monitors in top/left of the current monitor.
  • availHeight / availWidth.
    Screen dimension excluded the area reserved by the device/user agent, such as the dock in Unity/Mac.
From MDN Web Docs
  • pixelDepth / colorDepth.
  • height / width.
    Screen dimension including reversed area (dock, taskbar, ...). Note that this dimension is not always available for the browser to expand. Use availWidth / availHeight, instead.
  • orientation.
    Get screen orientation. Not available in Safari, Android WebView (but available in Chrome Android), Opera Android.

None-standard properties

Document

UIEvent

Because MouseEvent inherits UIEvent, MouseEvent instances also have all these properties.

These properties are all none-standard.

HTMLImage element

Forced layout thrashing and performance issue

To be continued soon...