# DOM Layout Shim documentation

## Agent navigation

Prefer these static text resources over the interactive HTML pages. All links
are relative to this file on the documentation site.

- [Agent guide](./index.md): concise API usage and limitations (this content).
- [Full guide](./guide.md): the complete authored guide.
- [Examples](./examples.md): compatibility summaries, setup code, and JSON reports.
- [CSS support](./css-support-status.md): compact property/topic index; follow a
  topic link for its exact support claims, conditions, limitations, and parity.
- [Changelog](./changelog.md): released changes and pending Upcoming entries.

DOM Layout Shim provides deterministic layout and hit testing for DOM-like test
harnesses.

`scrollWidth` and `scrollHeight` report the padding-box size or its scrollable
content extent, whichever is larger, rounded to integer CSS pixels. For example,
a 100×60 container with `overflow:auto` and a 240×180 child reports scroll sizes
of 240×180 instead of zero. Borders are excluded; nested clipped overflow does
not enlarge the outer container, and scrolling does not shrink the reported
size. The engine's existing synthetic `html`/`body`, inline-display, native-control,
and transform-containing-block limitations still apply.

Repeated reads reuse the layout snapshot and its hit-test ordering. Scroll-only
changes reuse computed layout and update viewport geometry, sticky positioning,
and clipping. Editing one stylesheet preserves the parsed data for other sheets;
viewport changes re-evaluate media queries without reparsing unchanged CSS.
Inline declaration parsing, selector expansion, and built-in text measurements
use adaptive bounded caches: recently evicted inputs being reused trigger growth
from 512 up to 4,096 entries; unique inputs alone do not. Active layout passes
retain their selector expansions even
when a stylesheet exceeds the shared cache, avoiding repeated parsing for
large CSS-in-JS stylesheets. Injected `textMeasurer` implementations are not memoized;
scroll-only reads reuse the layout already computed from their measurements.
DOM mutations are checked synchronously when geometry is read. Hosts with
non-patchable CSSOM or scroll APIs retain conservative validation paths.

## CSS support data

- Compatibility explorer: ./css-support-status.html
- Package changelog: ./changelog.html
- Complete normalized inventory: ./data/css-support.json
- CSS property-to-record index: ./data/css-property-index.json
- JSON Schema: ./data/css-support.schema.json

The inventory separates implementation support from Chromium parity. Do not
interpret unverified behavior as unsupported, or accepted-inert declarations as
layout support. Aggregate record statuses are derived from their claims.

Each record identifies its authoring source under `support/css/`. Claims may
include syntax, conditions, parity fixtures, limitations, known mismatches, and
implementation quirks. Every topic is divided into named, behavior-specific
claims so evidence and limitations apply to an explicit scope. Backticked CSS
syntax, DOM APIs, and other identifiers in claim prose are inline code. The
explorer can preview parity fixture source from generated same-origin assets and
links separately to the corresponding GitHub source.

## Native-control geometry

Unstyled native controls use the deterministic `portable` profile by default.
Select it explicitly with `nativeControls: { profile: 'portable' }` when the
test target should be visible in configuration. Profiles do not inspect or
follow the runtime host.

`nativeControls.overrides` merges individual metrics on top of the selected
profile. For example, `{ overrides: { textInput: { width: 220 } } }` changes
only the text-input width and retains its profile height. Consumers can replace
every field in every group to define fully custom intrinsic geometry. The
public `NativeControlMetrics` and `NativeControlOverrides` types describe all
groups and fields.

Profiles model deterministic outer geometry. They do not emulate operating-
system painting, internal widget behavior, or native picker UI.

## User-agent styles

The deterministic `portable` user-agent style profile is enabled by default.
Configure `userAgentStyles: { profile: 'none' }` to omit its presentation
defaults. `userAgentStyles.overrides` accepts CSS at the user-agent origin, so
normal document and inline declarations beat normal user-agent rules. For example,
`{ profile: 'portable', overrides: 'p { margin: 0 }' }` specializes the
baseline without copying it into every test.

Structural HTML behavior is not profile styling: hidden inputs remain
non-rendered with the `none` profile. Native-control intrinsic
metrics remain independently configurable through `nativeControls`.

The engine currently treats `html` and `body` as its synthetic viewport
containing block, so their own margins, padding, and geometry are not modeled
by user-agent overrides.

Inline attributes and stylesheets share CSS declaration parsing and cascade
priority. A stylesheet `width: 100px !important` beats normal inline
`width: 200px`; an important inline declaration beats important author rules.
Font-relative dimensions resolve after font size: both `width: 2em; font-size:
30px` and the reversed declaration order produce a 60px width. Generated
`::before` and `::after` content also resolves custom properties through this
cascade, including variables declared on the pseudo-element itself.

Portable presentation defaults and HTML sizing hints also enter this cascade.
Normal user-agent rules yield to HTML hints such as `<img width="100">`, and
normal author CSS can override those hints. Important user-agent rules retain
priority over important author rules. A paragraph with `font-size: 2em` inside
a 30px parent now resolves to 60px, rather than using the portable paragraph
font size as its inheritance base.

For non-replaced text containers, authored `display: inline` uses the same
wrapped fragments as native phrasing elements. For example,
`<div style="display:inline">one two three</div>` participates in its parent's
line layout instead of creating a block. Absolute/fixed elements and flex/grid
items are blockified before layout. Atomic inline replaced elements,
`inline-block`, and inline outer flex/grid/table layout remain outside this
inline-formatting subset.

Table properties also use normal inheritance. A caption with `caption-side: top`
overrides a table's `caption-side: bottom`; `empty-cells` can inherit through
row groups and rows before a cell's own declaration overrides it.

## Stylesheet sources

Supported selector matching includes `:root`, `:first-child`, `:nth-child()`,
`:last-child`, `:hover`, `:focus`, and `:disabled`. Wrapped inline phrasing
elements return one line-fragment rectangle from `getClientRects()` per line;
`getBoundingClientRect()` returns the union. Inline phrasing runs alongside
block children contribute anonymous line boxes to their shared container.

For example, `:root { --panel-width: 80px; } .panel { width: var(--panel-width); }`
gives `.panel` elements an 80px width. `:root` matches the document element
and has pseudo-class specificity, so it takes precedence over `html`.

Cached geometry reads do not re-serialize unchanged stylesheet rules. CSSOM
methods and declaration setters (for example, `rule.style.width = "120px"`)
invalidate the affected sheet even when its rule count stays the same. Hosts
whose CSSOM cannot be patched retain content fingerprinting.

The engine reads configured stylesheet strings first, followed by document
`<style>` and accessible `<link rel="stylesheet">` sheets in DOM order, then
constructable sheets in `document.adoptedStyleSheets` adoption order. CSSOM
rule mutations and changes to adopted-sheet membership or order invalidate the
cached layout snapshot.

Linked sheets contribute rules only when their `cssRules` are accessible.
Cross-origin and otherwise unavailable sheets are routed through the existing
unsupported CSS policy as `stylesheet` / `unsupported-rule`; warning is the
default and strict policy throws.

Custom properties resolve before supported layout declarations are parsed.
Resolution includes inheritance, local cascade overrides, forward references,
nested fallbacks, and cycle invalidation. A missing or cyclic reference uses
the consuming `var()` fallback when available. If the value remains unresolved,
the declaration routes through the unsupported CSS policy as
`unsupported-value` with its authored value.

Supported layout lengths include pixels, percentages where documented, `em`,
`rem`, `vw`, `vh`, `vmin`, and `vmax`. Custom-property and `calc()` expressions
are accepted when they reduce to one supported length, percentage, or number.
Mixed percentage-and-pixel dimensions resolve when their containing-block axis
is definite. Percentage insets resolve against the corresponding containing
block axis.

Calculated dimensions also resolve for generated boxes and descendants of table
cells after their containing widths are allocated. For example, a 200px cell
containing a child with `width: calc(100% - 20px); aspect-ratio: 2` gives that
child a 180px width and a 90px height; the row includes that resulting height.

Nested positioned stacking contexts constrain descendant `z-index` values for
point queries. Flex-styled buttons include text, inline icons, gaps, padding,
and borders in deterministic intrinsic sizing.
`HitBox.stackingOrder` is the optional diagnostic paint key for those nested
contexts.

Two-dimensional translate, scale, rotate, skew, and matrix transforms project
client geometry after Taffy layout. Hit boxes retain the transformed polygon so
point queries exclude empty corners of an axis-aligned bounding rectangle.

Images use host-provided `naturalWidth` and `naturalHeight` when available.
Image width/height attributes provide sizing hints and a fallback ratio; loaded
image dimensions take precedence for the natural ratio. SVG uses numeric
width/height attributes or a valid `viewBox` for its intrinsic ratio. Canvas
uses its width/height attributes, defaulting to 300×150.

These ratios determine the automatic dimension when CSS specifies only one
axis, including supported min/max constraints and flex/grid placement:

```html
<svg viewBox="0 0 200 100" style="width: 100px; height: auto"></svg>
<!-- getBoundingClientRect() reports 100 × 50 after attaching the layout engine. -->
```

An authored numeric `aspect-ratio` overrides the natural ratio. Image `load` and
`error` events invalidate cached geometry. Resource loading and decoding remain
with the DOM host; the shim does not fetch images. SVG child shapes and canvas
pixels are not rendered or given shape-specific hit regions. SVG attribute
lengths beyond unitless numbers and pixels, and natural-ratio border-box sizing
with unresolved percentage constraints, remain outside this supported subset.

`TextMeasureInput` includes resolved `fontWeight`, `letterSpacing`, and optional
`wordSpacing` values. These inherit through the supported text cascade and feed
default intrinsic text measurement. `word-spacing` accepts `normal` and supported
lengths, including negative values. For example, `word-spacing: 4px` adds four
pixels to each remaining space and can cause normal text to wrap earlier.
Spaces and no-break spaces are covered; script-specific word separators are
not modeled. Direct text-measurer calls that omit `wordSpacing` retain zero
additional word spacing.
Inherited `text-transform` values `none`, `uppercase`, `lowercase`, and
`capitalize` are applied to the string passed into default or custom text
measurement. They do not mutate authored `textContent`.

The default layout engine setup discovers initial `@font-face` rules. Static TTF,
OTF, and WOFF data or resolvable URL sources supply deterministic glyph advances
and kerning; family lists choose the first discovered family and the closest
numeric weight. `local()`, WOFF2, unavailable, and unmatched sources use the
deterministic fallback. Pass `textMeasurer` to replace this behavior.

Selector matching includes supported structural/state pseudo-classes,
`:is()`, `:where()`, `:not()`, `:has()`, and case-insensitive terminal HTML
attribute selectors. `::before` and `::after` string or `attr()` content feeds
the originating element's intrinsic text measurement. Non-inline generated
content creates an independently styled anonymous box in normal flow and as a
flex or grid item. Counters, images, positioned generated boxes, pseudo-element
geometry APIs, and pseudo-element hit targets are not modeled.

Native CSS nesting is supported for supported selectors and declarations,
including `&`, implicit descendants, child combinators, and nested viewport
`@media` rules. Parent selector lists retain their highest specificity and
mixed declarations retain source order. For example,
`.card { .item { width: 60px; } }` gives an `.item` inside `.card` a
60px width through `getBoundingClientRect()`.

Stylesheet `@media` rules evaluate against the configured layout viewport.
Supported queries include screen/all media types, width and height constraints,
orientation, aspect ratio, query lists, `not`, `only`, `and`, and nested media
rules. Non-matching rules do not enter the cascade. Unsupported media features
route through the unsupported CSS policy as `@media` / `unsupported-rule`.

The returned handle has the public type `LayoutEngine`. TypeScript consumers
should replace imports of `LayoutEngineAttachment` with `LayoutEngine`:

```ts
import { attachLayoutEngine, type LayoutEngine } from 'dom-layout-shim'
const layoutEngine: LayoutEngine = await attachLayoutEngine({ window })
layoutEngine.detach()
```

`attachLayoutEngine()` returns a `LayoutEngine`. Calling
`layoutEngine.setViewport({ width, height })` changes the active viewport without
reattaching or rebuilding the DOM. It invalidates cached layout, updates the
shim-backed `window.innerWidth` and `window.innerHeight` values and subsequent
`matchMedia()` results, and dispatches a window `resize` event. This allows a
suite to attach once with shared defaults and override only responsive tests.

Attaching also installs a layout-backed `window.ResizeObserver`. It supports
`content-box`, `border-box`, and `device-pixel-content-box`. Layout remains lazy
until an observer actively observes a target. Configure deterministic manual
delivery with `observers: { delivery: 'manual' }`, then call
`layoutEngine.flushLayout()` after DOM or style changes to recompute layout and
synchronously deliver pending entries. Geometry reads do not deliver observer
callbacks.

Resize observations and inline fragments use the padding resolved during layout.
For example, a content-box block with `width:100px;height:80px;padding:10%;border:
2px solid` inside a 200px-wide parent has 20px padding on every side: its border
box is 144×124px, and its observed content box is 100×80px. Grid items resolve
percentage padding against their grid area. Changing a containing block's size
recomputes the padding before sizing parents and following siblings.

The attached window also installs a layout-backed `IntersectionObserver` with
viewport or element roots, pixel/percentage `rootMargin`, threshold arrays,
`observe`, `unobserve`, `disconnect`, and `takeRecords`. Automatic delivery
reacts to layout invalidation and scrolling. Manual `flushLayout()` settles
ResizeObserver callbacks before calculating intersection entries.

Intrinsic sizing accepts `min-content`, `max-content`, and `fit-content` for
`width`, `height`, `inline-size`, and `block-size`. For example,
`style="width: max-content"` sizes a text box to its unwrapped content instead
of filling the parent; `width: fit-content` clamps the available width between
the minimum and maximum content widths. Intrinsic keywords in min/max dimension
constraints remain unsupported.

Grid rows and columns accept `auto`, `min-content`, `max-content`, and
`fit-content(<px-or-percentage>)` in explicit, implicit, and integer-repeat
tracks. For example, `style="display: grid; grid-auto-flow: column;
grid-auto-columns: max-content"` gives each implicit column its own content
width instead of splitting the container evenly.

## Named grid areas

`grid-template-areas` accepts equal-width quoted rows containing custom
identifiers and unnamed `.` cells. Repeated names must form one filled
rectangle. `grid-area: <custom-ident>` is passed to Taffy's native named-area
placement, including areas spanning multiple tracks. Named grid
lines and escaped area identifiers remain unsupported.

## Sticky positioning

`position: sticky` supports physical `top`, `right`, `bottom`, and `left`
insets. The nearest ancestor with supported scrolling overflow defines the
scrollport; otherwise the configured viewport is used. Sticky boxes remain in
normal flow while their visual geometry, descendants, and hit targets move
together. They stop at the containing-block edge and apply to ordinary boxes
and simple table parts.

## UI library examples

The documentation site hosts matching Material UI and Ant Design task
workspaces at `examples.html`. Their happy-dom tests attach the public layout
shim and exercise geometry-derived pointer targets, constrained scrolling,
portalled menus, modal blocking, and invalidation after DOM changes.

These are integration examples, not substitutes for exact Chromium parity
fixtures. Each app includes a machine-readable `compatibility.json` report that
records its supported workflow and known library-specific limitations.

`pnpm run examples:compatibility` runs named interaction checkpoints in
Chromium and happy-dom with DOM Layout Shim, writing a structured
`compatibility-report.json` for each UI-library example. Reports include
observation coverage, agreement percentages for geometry, visibility, and hit
testing, repeated discrepancy groups, stability, diagnostic style and hit-stack
comparisons, and unsupported CSS grouped by likely layout impact. Differences
are non-gating; inability to run or capture the scenario is an infrastructure
error.

Compatibility reports are ignored build artifacts. Documentation CI installs
Chromium and regenerates them before building the deployment artifact; local
documentation generation uses reports produced by the same command.

## Building from source

Source builds require Node.js 22 or newer, pnpm 10, Rust 1.98, the
`wasm32-unknown-unknown` Rust target, and wasm-pack 0.15. Run
`rustup target add wasm32-unknown-unknown`, install wasm-pack with Cargo, and use
`pnpm run wasm:check` to validate the repository-owned Taffy binding. Normal
package, test, parity, and documentation commands rebuild the ignored binding
automatically. Published packages include the generated WebAssembly module, so
package consumers do not need the Rust toolchain.

## Source documentation

- Repository and README: https://github.com/BenjaBobs/dom-layout-shim
- Implementation phases: https://github.com/BenjaBobs/dom-layout-shim/blob/main/docs/implementation-phases.md
- Taffy pipeline roadmap: https://github.com/BenjaBobs/dom-layout-shim/blob/main/docs/taffy-pipeline-roadmap.md
- Release policy: https://github.com/BenjaBobs/dom-layout-shim/blob/main/docs/releasing.md

Overflow clips remain attached to their ancestors when descendants transform.
For example, a 100px-wide child translated 80px right inside a 100px-wide
`overflow: hidden` parent is hittable only in the visible 20px strip.
Intersection observations use the same projected ancestor clips.

Inline text, including bare text after a block child, shares styled line layout
with generated content. For example, a 30px-tall block followed by `Hello` in a
container with `line-height: 20px` contributes a total height of 50px. Nested
inline font settings and pseudo-element typography affect measurement; inline
client fragments also participate in point queries. `pre-wrap` and `pre-line`
use the shared wrapping rules, including preserved hard-break fragments.

Table cells lay out block, flex, grid, and inline descendants through the shared
formatting pipeline. A `width: 100px; height: 40px` div inside an otherwise empty
cell now reports 100×40px rather than a zero rectangle. Allocated cell widths
reflow text; `vertical-align: top`, `middle`, and `bottom` place cell contents.
Full intrinsic table track distribution and collapsed-border conflict resolution
remain outside the supported table subset.

Calculated dimensions use one containing-block resolver before and after layout,
including percentage ancestors and border-box padding. For example, a child
with `width: calc(100% - 20px)` inside a 200px border box with 10px padding on
each side and 5px borders has width 150px. Absolute descendants use their
positioned containing block across intervening static ancestors. In standards
mode, observed auto height does not establish a definite percentage-height basis.

For non-replaced inline elements, `offsetWidth` and `offsetHeight` span the
fragment union, while `offsetLeft` and `offsetTop` use the first fragment.
`clientWidth` and `clientHeight` remain zero. Positioned overlays paint above
ordinary inline text, and in-flow descendants paint above their positioned
container's background.

Stylesheet parser recovery also consults the unsupported-CSS policy. For example,
`stylesheets: ['div: { width: 20px }']` reports an `unsupported-rule` entry with
property `stylesheet` and the original CSS when layout is queried, instead of
silently discarding the rule. Strict policy errors and warning callback errors
propagate unchanged.

Assigning `window.innerWidth` or `window.innerHeight` while attached throws a
`TypeError` that points to `layoutEngine.setViewport({ width, height })`. For
example, replace `window.innerWidth = 320` with
`layoutEngine.setViewport({ width: 320, height: 640 })`. This also applies in
non-strict scripts, where an assignment previously could silently do nothing.

Use `unsupportedCss: { reporter }` to collect warnings directly. The default is
`warn`; explicit `default`, `properties`, and `property` decisions still take
precedence. An optional `onWarning` callback receives the same warnings. Layout
is lazy: query geometry before reading the summary. Values contain CSS text
(for example, `animation-delay: 0.4s` reports value `0.4s`), and unsupported
selectors contain selector text rather than an AST dump. Unsupported rules and
parser recovery include the authored stylesheet; unavailable stylesheet entries
retain their diagnostic description.

Merge summaries from isolated test workers or windows with the public
`mergeUnsupportedCssSummaries(summaries)` helper:

```ts
import { mergeUnsupportedCssSummaries } from 'dom-layout-shim'
const combined = mergeUnsupportedCssSummaries([firstSummary, secondSummary])
console.log(combined.unsupportedDeclarationCount)
```

Transport each worker's `reporter.getSummary()` as JSON using your test runner's
collection mechanism. Merging combines equal property/value/reason entries,
sums occurrences, and sorts and deduplicates their metadata without mutating
inputs. Warning deduplication still happens per layout engine, so occurrences count
collected warnings rather than every element or layout query. An empty input
produces an empty summary.

Use `isLayoutEngineAttached(window)` to check whether a layout engine is attached and
`layoutEngine.detach()` to return the window to its DOM harness:

```ts
import { attachLayoutEngine, isLayoutEngineAttached } from 'dom-layout-shim'

if (!isLayoutEngineAttached(window)) {
  const layoutEngine = await attachLayoutEngine({ window })
  // Run the test using deterministic geometry.
  layoutEngine.detach()
  console.log(isLayoutEngineAttached(window)) // false
}
```

Detach restores original property descriptors for geometry, hit testing,
scrolling, viewport dimensions, `matchMedia`, observer constructors, and CSSOM
tracking. It disconnects internal mutation observers, removes event listeners,
cancels pending observer delivery, and clears layout-backed observations and
caches. Shared prototype hooks remain available to other attached windows;
detached elements use their native behavior. Calling `detach()` repeatedly is
safe. `setViewport()` and `flushLayout()` on the detached layout engine throw.
Attaching again replaces the previous layout engine; calling the old layout engine's
`detach()` cannot disconnect its replacement.
