Web components that measure
What a composition's DOM answers, as Chrome does: refs' geometry, style and tree, document and window, canvas and svg text measures, Range, ResizeObserver, and the frame held until a measure lands. And what it lacks.
Many components written for a web page measure before they draw: a title sized to fit its box, captions that wrap at
the width a line really takes, a caret placed after the last typed letter. In a composition those reads answer as they
do in Chrome, from the same layout and the same font files that draw the frame. Nothing needs importing: the entry
installs them on every ref and on document and window.
Measure, then let the frame be taken
A measure runs in an effect, after the first commit. Hold the frame until it's done, so the frame taken is the measured one:
import { continueRender, delayRender } from "raea";
import { loadFont } from "raea/text";
loadFont("Inter");
export const Fit = ({ text }: { text: string }) => {
const ref = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const [handle] = useState(() => delayRender("fit the title"));
useEffect(() => {
document.fonts.ready.then(() => {
setScale(1080 / ref.current!.getBoundingClientRect().width);
continueRender(handle);
});
}, []);
return <div ref={ref} style={{ display: "inline-block", fontFamily: "Inter", transform: `scale(${scale})` }}>{text}</div>;
};A hold never released fails the render after 30 s, naming its label (raea/hold). Load a
web font with loadFont so the measure uses its own faces; a family you never asked for is measured in raea's bundled
faces, as a browser falls back.
What a ref answers
| Geometry | getBoundingClientRect() (transforms included), getClientRects(), offsetWidth, offsetHeight, offsetLeft, offsetTop, offsetParent, client*, scroll*. Values are Chrome's, in the composition's CSS px. |
| Style | el.style reads what React committed. Writing it (a property, setProperty, cssText) restyles the element at once; React's next commit to the same property wins. getComputedStyle(el) answers from the page's computed style. |
| The tree | children, childNodes, the sibling and parent links, tagName, id, className, classList, dataset, getAttribute, textContent, contains, matches, closest, querySelector and querySelectorAll. |
| svg text | getComputedTextLength, getNumberOfChars, getStartPositionOfChar, getEndPositionOfChar and getSubStringLength, in the element's computed font, letter- and word-spacing and font-variation-settings included, hidden text too. |
| canvas | getContext("2d") with measureText (width, the ink's and the font's boxes, shaped from the faces the canvas draws with), letterSpacing and wordSpacing, getImageData and putImageData. |
| images | An <img>'s onLoad fires once it's made or its src changes; the frame still waits for the picture itself. |
Selectors cover tag, *, #id, .class, [attr] and [attr=v] (and ~=, ^=, $=, *=), the descendant and
child combinators, comma lists, :first-child, :last-child, :only-child, :nth-child() and :not(). Anything
else throws a SyntaxError, as Chrome's does.
document and window
document.bodyis the composition's root.getElementById,querySelectorandquerySelectorAllsearch it.document.createElementmakes an element of the page: appended into the composition it's laid out, measured and drawn, and removed it's gone. That's how a component measures a hidden copy. A canvas made there is an OffscreenCanvas with the same 2D context.document.createRange()over a text node gives the rect of its characters, placed where the line starts. One line only: a range across wrapped lines gets one rect, the width of its characters.document.fontshasready,loadandcheck; aFontFacegiven by URL is fetched and its text set in it. One that doesn't load rejects, and the text falls back.windowhasinnerWidth,innerHeightanddevicePixelRatiofor the composition's size, andgetComputedStyle.requestAnimationFramecalls back once per frame drawn, given that frame's time, so a frame drawn again gets the same calls.new Image()firesloadwhen itssrcis set, with no pixel size.ResizeObservercalls back once when it starts observing, then after any layout that changed an observed element's size, withcontentRect,borderBoxSizeandcontentBoxSize.
What it lacks
MutationObserverandIntersectionObserverexist, so a component that makes one doesn't throw, but they never call back.- An
<img>'sonErrornever fires: an image that doesn't load fails the render, naming its src. new Image()has nonaturalWidthornaturalHeight(they stay 0).- An element has no
innerHTMLorouterHTML: readtextContent, or walkchildrenandchildNodes. - A Range across wrapped lines is one rect, not one per line, and an inline element's
getClientRects()is one rect too. - In a pass that draws no page (the sound pass, a 3D pre-pass) every read answers 0, and holds there are released.
- Inside a
RuneLayerin an app,getBoundingClientRectis zero.
Measured against Chrome on snapcn's measuring components, bench/layout-reads/ in the repository is the harness that
holds these to it (#54).