css-box-model
css-box-model is a small browser utility that turns an element's bounding rectangle and computed CSS into named margin, border, padding, and content rectangles. Unlike getBoundingClientRect(), which gives only the border box, its getBox() result includes coordinates, dimensions, centers, and the four edge widths for every layer. It also exposes pure geometry helpers, so drag-and-drop and positioning code can adjust rectangles without repeatedly reading layout from the DOM.
A focused and pleasant geometry helper for old or existing drag-and-drop code, with an API small enough to audit. For a new app that needs only one rectangle, use the DOM directly; for full placement behavior, use Floating UI.
Use it if
- You are writing drag-and-drop, collision, or positioning logic that repeatedly needs all four CSS box layers
- You want one clearly named result instead of combining getBoundingClientRect() with a dozen getComputedStyle() properties
- You already have a DOMRect and CSSStyleDeclaration and want to calculate the remaining rectangles once
- You need pure helpers for expanding, shrinking, or offsetting rectangle-like objects
- You only need an element's visible border rectangle: getBoundingClientRect() is native, dependency-free, and already returns that box
- You need layout measurements in Node or server rendering: getBox() reads window.getComputedStyle() and an Element, so it is a browser API despite the package being importable elsewhere
- You measure hidden elements or styles that resolve to rem, em, or an empty string: the source deliberately converts every computed value without a px suffix to zero
- You need active product development: version 1.2.1 was published in April 2020 and the repository's last push was in January 2023
- You need collision placement, flipping, clipping, or middleware rather than raw rectangles: @floating-ui/dom supplies those higher-level positioning decisions
Setup reality
Installation is just npm install css-box-model. Version 1.2.1 has one runtime dependency, tiny-invariant, and ships CommonJS, ES module, Flow, and declaration-file entry points. There are no peer dependencies, build tools, credentials, or configuration files. The important setup constraint is the runtime: getBox() calls Element.getBoundingClientRect() and window.getComputedStyle(), while withScroll() reads window.pageXOffset and window.pageYOffset when no scroll value is passed. Keep those calls in browser-only code and after the element is mounted; a server render, a Node process, or a test environment without DOM shims cannot produce real measurements. jsdom does not perform layout and often returns zero or empty computed values, so unit tests should prefer createBox() or calculateBox() with explicit fixtures. The source accepts only computed spacing strings ending in px; rem, em, auto, and missing values become zero instead of throwing. One README sample says window.getComputedStyles(), but the actual code correctly uses window.getComputedStyle(). Finally, every getBox() call performs a geometry read and a style read. Batch measurements before DOM writes in animation or drag loops to avoid forced layout, and use calculateBox() when you already fetched the rect and styles.
Patterns
Read every box layer from an elementmeasure-element
import { getBox } from 'css-box-model';
const element = document.querySelector('[data-card]');
if (!(element instanceof HTMLElement)) throw new Error('card missing');
const box = getBox(element);
console.log(box.marginBox, box.borderBox, box.paddingBox, box.contentBox);Call this only after the element is mounted and visible; it reads both layout and computed styles.
Get the content area's size and centerread-content-size
const { contentBox } = getBox(element);
console.log({
width: contentBox.width,
height: contentBox.height,
centerX: contentBox.center.x,
centerY: contentBox.center.y,
});Coordinates are viewport-relative because the underlying border rectangle comes from getBoundingClientRect().
Read margin, border, and padding widthsinspect-spacing
const box = getBox(element);
console.log('margin top', box.margin.top);
console.log('border left', box.border.left);
console.log('padding right', box.padding.right);Each spacing object has top, right, bottom, and left values in pixels; non-pixel computed strings become zero.
Convert viewport boxes to page coordinatesconvert-page-coordinates
import { getBox, withScroll } from 'css-box-model';
const viewportBox = getBox(element);
const pageBox = withScroll(viewportBox);
console.log(pageBox.borderBox.top);withScroll() reads window.pageXOffset and pageYOffset by default, so do not call it during server rendering.
Apply an explicit scroll offsetapply-known-scroll
const pageBox = withScroll(viewportBox, {
x: 0,
y: 640,
});Passing the position avoids a window read and makes the operation deterministic in tests.
Calculate from a rectangle and styles you already readreuse-dom-reads
import { calculateBox } from 'css-box-model';
const rect = element.getBoundingClientRect();
const styles = window.getComputedStyle(element);
const box = calculateBox(rect, styles);Use this when other code already needs the same DOMRect and CSSStyleDeclaration; it avoids fetching them twice.
Create a box model without reading the DOMcreate-fixture
import { createBox } from 'css-box-model';
const box = createBox({
borderBox: { top: 10, right: 210, bottom: 110, left: 10 },
padding: { top: 8, right: 8, bottom: 8, left: 8 },
border: { top: 1, right: 1, bottom: 1, left: 1 },
});Omitted margin, border, or padding values default to zero on all four edges.
Turn four edges into a full rectanglenormalize-rectangle
import { getRect } from 'css-box-model';
const rect = getRect({ top: 20, right: 180, bottom: 100, left: 40 });
console.log(rect.width, rect.height, rect.x, rect.y, rect.center);Width is right minus left and height is bottom minus top; reversed edges therefore produce negative sizes.
Expand a rectangle for hit testingexpand-hit-area
import { expand, getRect } from 'css-box-model';
const hitArea = getRect(expand(box.borderBox, {
top: 8, right: 8, bottom: 8, left: 8,
}));expand() returns only top, right, bottom, and left; pass the result through getRect() when you need width, height, or center.
Inset a rectangle on every edgeshrink-rectangle
import { shrink, getRect } from 'css-box-model';
const inner = getRect(shrink(box.borderBox, {
top: 4, right: 4, bottom: 4, left: 4,
}));Large inset values can cross the edges and produce a negative width or height; the helper does not clamp.
Move a complete box modeloffset-box-model
import { offset } from 'css-box-model';
const moved = offset(box, { x: 24, y: -12 });
console.log(moved.borderBox.left, moved.contentBox.top);offset() rebuilds every layer from the shifted border box while preserving the original spacing values.
Annotate stored measurements in TypeScripttype-box-result
import { getBox, type BoxModel } from 'css-box-model';
let previous: BoxModel | null = null;
previous = getBox(element);Version 1.2.1 includes its own declaration file, so no @types package is required.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @floating-ui/dom | npm | Choose it when you need popover placement, clipping, flipping, and middleware rather than only box arithmetic |
| get-css-data | npm | Choose it when the task is reading a broader set of computed CSS properties from an element |
| compute-scroll-into-view | npm | Choose it when the end goal is calculating scroll actions that reveal an element |