Element Geometry and Scrolling
Measure elements, scroll containers, and find elements under the cursor from native code.
On this page
You can measure page elements, scroll containers, and find elements under the cursor directly from native code. The DOM bridge provides the same geometry calls as JavaScript, making it straightforward to align native UI with rendered content and route mouse clicks.
Measuring an Element
Calling getBoundingClientRect() on an element returns its position and size relative to the viewport in CSS pixels:
auto minimap = document.getElementById("minimap");
dom::DOMRect box = minimap.getBoundingClientRect();
double scale = view->device_scale();
DrawOverlay(box.x * scale, box.y * scale, box.width * scale,
box.height * scale);
All coordinates in the returned dom::DOMRect are zero if the element isn't displayed.
📘 Viewport and View Pixels
Page coordinates use CSS pixels. Multiply CSS pixels by
View::device_scale()to get View pixels, or divide View pixels by the scale factor to convert back. For example, an 800x600 View with a device scale of 2.0 has a 400x300 CSS pixel viewport.
Box Metrics
Other element methods return dimensions in whole CSS pixels, returning zero if the element isn't displayed:
| Method | Measures |
|---|---|
offsetWidth(), offsetHeight() |
Total size, including padding and borders |
offsetTop(), offsetLeft() |
Distance from offsetParent() (the nearest positioned ancestor) |
clientWidth(), clientHeight() |
Inner size, including padding but excluding borders and scrollbars |
clientTop(), clientLeft() |
Width of the top and left borders |
scrollWidth(), scrollHeight() |
Full content size, including areas scrolled out of view |
🚧 Layout Recalculation
Reading any measurement forces an immediate style and layout update if the page has pending changes. Make all DOM modifications first, then read element dimensions in a single pass.
Inspecting the Viewport
Calling document.defaultView() returns the page's dom::Window, which provides the viewport dimensions:
dom::Window window = document.defaultView();
FitHud(window.innerWidth(), window.innerHeight(), window.devicePixelRatio());
innerWidth() and innerHeight() report the viewport size in CSS pixels. devicePixelRatio() returns the ratio of device pixels to CSS pixels, which matches View::device_scale().
Scrolling an Element
To scroll an element's content, call scrollTo() or scrollBy() on the element:
auto inventory = document.getElementById("inventory");
inventory.scrollTo(0, 0);
inventory.scrollBy(0, 48); // down one row
selected.scrollIntoView();
You can also assign directly to scrollTop or scrollLeft to set the scroll offsets.
Calling scrollIntoView() scrolls parent containers until the element becomes visible.
All scrolling in Ultralight is instant— CSS scroll-behavior is ignored, and target positions clamp automatically to the scrollable range.
Scrolling the Page
To scroll the entire page, call scrollTo() or scrollBy() on the window:
window.scrollTo(0, 0);
double y = window.scrollY();
Window scrolling follows the same rules— movement is instant and clamped to the page boundaries.
You can read the current viewport scroll offsets in CSS pixels using scrollX() and scrollY().
Finding Elements Under a Point
To inspect what lies under the cursor, pass viewport coordinates to elementsFromPoint() on the document:
bool IsOverUI(const dom::Document& document, double x, double y) {
for (auto el : document.elementsFromPoint(x, y)) {
if (!el.matches("html, body, .hud-layer"))
return true; // a UI element is under the cursor
}
return false; // only empty layers: the click goes to the game
}
Calling elementFromPoint() returns only the topmost element at that point, or an empty element if the coordinate lies outside the viewport.
elementsFromPoint() returns all elements at that point from topmost to bottom, ending with the root <html> element.
Both methods expect CSS pixels relative to the viewport— the same coordinate space used by getBoundingClientRect() and mouse event client positions.
Reacting to Window Resizes
To update native UI when the viewport size changes, attach a resize listener to the window:
window.addEventListener("resize", [window] {
FitHud(window.innerWidth(), window.innerHeight(),
window.devicePixelRatio());
});
Window listeners receive resize events when the View next paints, rather than during the call to View::Resize(). Document listeners never receive resize events.
Viewport scroll events follow the same schedule— they fire on the document and bubble to the window.
To learn more about listening for window events, see Handling DOM Events.
Differences from JavaScript
While DOM geometry in Ultralight mirrors JavaScript, several methods and properties differ in C++.
| JavaScript | C++ |
|---|---|
Options objects like scrollTo({ behavior: "smooth" }) and scrollIntoView({ block: "center" }) |
scrollTo(x, y) takes numbers only, and scrollIntoView() takes a boolean flag (align_to_top). Scrolling is always instant |
Properties rect.left, rect.right, rect.top, and rect.bottom |
Methods left(), right(), top(), and bottom() (x, y, width, and height remain struct fields) |
Read-only metrics as properties (el.offsetWidth, window.innerWidth, window.scrollY) |
Getter methods (offsetWidth(), innerWidth(), scrollY()) |