LOKE Design System
Lib

Rect Utility — observeElementRect

Observe changes to an element’s bounding client rect with a lightweight, batched requestAnimationFrame loop.

observeElementRect

observeElementRect tracks an element’s position and size on screen (via getBoundingClientRect) and calls your callback whenever it changes. It batches DOM reads/writes in a requestAnimationFrame loop, only running while at least one element is being observed.

This utility is great for anchoring floating UI (tooltips, popovers, menus), scroll‑based effects, or reactive layout adjustments without setting up your own observer loop.


Features

  • Efficient DOM reads: uses a single requestAnimationFrame loop for all observed elements
  • Change detection: invokes callbacks only when the rect actually changes
  • Automatic lifecycle: the loop starts with the first observation and stops when there are no observers
  • Multiple callbacks per element: the same element can have multiple listeners

API

type Measurable = { getBoundingClientRect(): DOMRect };

/**
 * Observe an element's bounding client rect and receive updates on change.
 * Returns a cleanup function to stop observing.
 */
function observeElementRect(
  elementToObserve: Measurable,
  callback: (rect: DOMRect) => void
): () => void;

Prop

Type

Return value:

  • A function that removes this callback and, if no callbacks remain for the element, stops observing it. The global loop stops automatically when there are no observed elements left.

Usage

Basic

import { observeElementRect } from '@loke/ui/rect';

const el = document.getElementById('my-element')!;
const stop = observeElementRect(el, (rect) => {
  console.log('Rect changed:', rect);
});

// Later, to stop:
stop();

With React refs

import * as React from 'react';
import { observeElementRect } from '@loke/ui/rect';

export function AnchoredBadge() {
  const ref = React.useRef<HTMLDivElement | null>(null);
  const [rect, setRect] = React.useState<DOMRect | null>(null);

  React.useEffect(() => {
    if (!ref.current) return;

    const stop = observeElementRect(ref.current, (next) => {
      setRect(next);
    });

    return stop;
  }, []);

  return (
    <div className="relative">
      <div ref={ref} className="inline-block rounded border bg-card px-4 py-2">
        Anchor
      </div>

      {rect ? (
        <div
          className="absolute rounded-full bg-primary px-2 py-1 text-primary-foreground text-xs"
          style={{
            // Example: place badge near the bottom-right corner of the anchor
            transform: `translate(${rect.width - 10}px, ${rect.height - 10}px)`,
          }}
        >

        </div>
      ) : null}
    </div>
  );
}

Multiple listeners for the same element

import { observeElementRect } from '@loke/ui/rect';

const el = document.querySelector('#observe-me')!;
const stopA = observeElementRect(el, (rect) => console.log('A:', rect.width));
const stopB = observeElementRect(el, (rect) => console.log('B:', rect.height));

// Unsubscribe one listener:
stopA();
// The element keeps being observed while stopB is still active.

How it works

  • The first time an element is observed, it’s added to an internal map with its callbacks and last known rect.
  • A single requestAnimationFrame loop:
    • Reads: calls getBoundingClientRect() for all observed elements and compares with the last cached rect.
    • Writes: invokes callbacks only for elements whose rect changed.
  • The loop starts when the first element is observed, and stops when there are no observed elements.
  • If you add a new callback to an already observed element, that callback is invoked immediately with the current rect.

Performance

  • Batches DOM reads first, then batches callback invocations (writes), minimizing layout thrashing.
  • Only fires your callback on actual changes to the rect (top/right/bottom/left/width/height).
  • The loop automatically stops when not needed.

For most UI use cases (tooltips, popovers, sticky elements), this approach is performant and simple. If you need resize‑only tracking, consider ResizeObserver — but note it won’t fire on scroll/position changes.


Use cases

  • Positioning floating UI relative to anchors across scroll and layout changes
  • Scroll‑based animations that depend on element position/visibility
  • Reactive layouts that need the latest measured size or position
  • Building custom viewport-aware components

Tips and edge cases

  • Ensure your element still exists when creating the observer (e.g., after mount).
  • If you observe something that implements getBoundingClientRect (custom Measurable), keep it stable for accurate updates.
  • When removing elements, call the cleanup function to stop tracking if you no longer need updates.
  • For many small/dynamic elements, consider scoping observations to what’s on screen to reduce work.

Types

type Measurable = { getBoundingClientRect(): DOMRect };
type CallbackFn = (rect: DOMRect) => void;

// observeElementRect(
  // elementToObserve: Measurable,
  // callback: CallbackFn
// ): () => void

This utility offers a simple, robust approach to tracking element geometry that plays nicely with React and typical floating‑UI patterns.