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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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](/docs/2.0/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()`) |
