OverflowLint Try the demo →

Runtime layout checks for rendered UIs.

OverflowLint turns live DOM geometry and browser hit testing into structured, actionable findings for developers, Playwright, and coding agents.

It detects document overflow, masked clipping, truncated text, covered controls, and undersized interactive targets on ordinary rendered pages. Baseline checks need no annotations. Optional data-ol-* contracts add product-specific intent.

Website · Interactive demo · Generated reference

Install

pnpm add -D overflowlint

The package is ESM. playwright is an optional peer and is only needed for the overflowlint/playwright adapter.

One rendered-page scan

import { OverflowLint } from 'overflowlint'

const result = OverflowLint.run({
  tolerance: 1,
  max_findings: 80,
  report_to_console: true,
})

if (result.summary.findings.error > 0) {
  console.error(JSON.stringify(result.findings, null, 2))
}

Every result is serializable and includes schema_version: 2, runtime metadata, raw findings, root-cause groups, skipped-check counts, active contracts, suppressions, and suppression-policy violations. Every finding has a closed rule, stable fingerprint, category, static rule level, dynamic confidence, detection level, rectangle, typed evidence, and deterministic diagnosis/fix candidates.

Rule levels are essential, recommended, and experimental. They describe the maturity and intended future preset of a rule, not the certainty of one finding. All levels currently run by default. A finding's confidence remains the page-specific certainty signal.

Playwright: inject, scan, verify

Playwright injection is the primary test integration. check_page() installs the shipped panel-free core if necessary, waits for fonts and images, and lets the page settle: two animation frames plus any in-flight finite CSS transitions/animations (polled via document.getAnimations(), capped at 1s so spinners or looping effects never block a scan). It reuses an app-provided runtime when its schema_version is compatible.

Settling is configurable with settle: { animations: { max_wait_ms } } or disabled with settle: { animations: false }. Because findings depend on the current scroll position, check_page() also records it in report.scroll; pass scroll_to_top: true to reset to the top before scanning for reproducible results.

import { expect, test } from '@playwright/test'
import { check_page } from 'overflowlint/playwright'

test('has no high-confidence layout errors', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 })
  await page.goto('http://localhost:5173/example')

  const report = await check_page(page, {
    failure_policy: {
      severities: ['error'],
      minimum_confidence: 'high',
    },
  })

  expect(report.finding_failures, JSON.stringify(report, null, 2)).toEqual([])
  expect(report.suppression_failures).toEqual([])
})

inject_lint(page, { force?: boolean }) supports strict-CSP pages. A compatible runtime is reused; an incompatible one is rejected unless forced replacement is explicit. Suppression failures stay separate from DOM findings, so no fake zero-rectangle issue is created.

An application-owned watcher remains a supported alternative:

import { OverflowLint } from 'overflowlint'

const stop = OverflowLint.watch({
  report_to_console: false,
  on_result(result) {
    console.log(result.summary.groups.actionable)
  },
})

stop()

Only one watcher is active. While an ESM watcher runs, the singleton is attached to window.OverflowLint for browser tools; stop() removes that temporary global. The IIFE and userscript always expose the global.

Text robustness

Ordinary scans report observed failures and masked failures already present in the DOM. Text stress is explicit and asynchronous:

import { OverflowLint } from 'overflowlint'

const stressed = await OverflowLint.stress_text({
  profiles: ['expanded', 'unbroken', 'mixed'],
  max_candidates: 40,
  max_outcomes: 20,
})

console.log(stressed.stress?.passed)

The default profiles cover expanded copy, an unbroken token, and mixed Latin, CJK, emoji, and numeric text. Atomic url, numeric, emoji, and cjk profiles are also available. The scan mutates one visible text node at a time, waits for layout, records only new failures, and always restores the original text. Ellipsis and line-clamp behavior introduced by stress is returned as bounded graceful-degradation outcomes rather than failures. Detection levels are observed, masked, and text-stress.

Experimental runtime audits

Experimental rules currently run alongside the established rule set and emit mostly medium- or low-confidence warnings. They cover zero-area, clipped, transparent, pointer-disabled, and offscreen fixed controls; horizontal viewport escape; flex/grid item overflow; sibling content overlap; fixed/sticky obstruction; and informational DOM/z-index complexity.

Core findings include typed likely causes and fix candidates. A candidate may carry preview-safe CSS declarations such as min-width: 0 or overflow-wrap: anywhere; OverflowLint reports these declarations but never applies them automatically.

Optional contracts

Contracts are a second layer for intent the browser cannot infer reliably:

<main data-ol-no-scroll="x">
  <header data-ol-in-viewport>
    <button aria-label="Open navigation" data-ol-min-target="44">Menu</button>
  </header>

  <aside data-ol-min-visible="0.75" data-ol-contained-by="viewport">
    Account status
  </aside>

  <div data-ol-allow-scroll="x">
    <!-- Intentional horizontal scrolling -->
  </div>

  <nav data-ol-overlay-chrome>
    <!-- Persistent app chrome: never reported as covering content -->
  </nav>

  <aside data-ol-off-canvas>
    <!-- Hidden off-canvas drawer, e.g. translateX(-105%) -->
  </aside>
</main>

Prefer narrow allowances. data-ol-ignore removes an entire subtree from findings and hit-test occlusion, so suppression policies should audit broad exceptions in automated tests. Results include an educational warning when an ignore subtree is broad; prefer an axis-specific or intent-specific contract when possible. data-ol-overlay-chrome marks persistent app chrome whose hit-test occlusion is intentional — it stops being reported as a covering element, but its own subtree still runs every check. data-ol-off-canvas declares an intentionally off-canvas element (hidden drawer) so it is not reported as a horizontal scroll-container leak; a position: fixed element whose box is fully outside the viewport on the x-axis is inferred the same way.

The complete contract and rule tables are generated from the source registries in docs/reference.md.

Rich diagnostics and viewport matrices

inspect_page() defaults to deep, agent-oriented diagnostics. Each selected finding includes its semantic ancestor chain (including aria-hidden, inert, roles, modal state, and overflow styles) and interactive classification when applicable. The report also identifies the core runtime and Playwright adapter versions. Use diagnostics: 'failures' for the compact failure-only shape, or diagnostics: 'none' to disable enrichment.

import {
  attach_lint_report,
  check_viewports,
  inspect_page,
} from 'overflowlint/playwright'

const report = await inspect_page(page, {
  diagnostics: 'deep',
  text_stress: true,
  fail_on_suppression_violations: true,
  lint_options: {
    suppression_policy: { disallow: ['ignore'], max: 2 },
  },
})

await attach_lint_report(testInfo, page, report, {
  // The PNG includes numbered, severity-colored failure boxes.
  screenshot: { annotate: true, max_findings: 20 },
})

const matrix = await check_viewports(
  page,
  [
    { name: 'mobile', width: 390, height: 844 },
    { name: 'desktop', width: 1440, height: 900 },
  ],
  {
    diagnostics: 'failures',
    failure_policy: { severities: ['error', 'warning'] },
  },
)

console.log(matrix.failures)

inspect_page() can add computed styles, hit-test stacks, text rectangles, ancestors, and likely overflow culprits. check_viewports() groups the same fingerprint across named viewports. Matrix runs scan from the top by default so responsive reflow cannot make later runs inherit a different scroll position; the original viewport and scroll position are restored afterwards. Set scroll_to_top: false only when intentionally testing the current scroll state. attach_lint_report() annotates screenshot attachments by default and removes its isolated overlay immediately after capture. Pass screenshot: { annotate: false } when a clean page image is preferable.

One horizontal overflow no longer cascades into a finding for every ancestor up to <html>. Unclipped inferred-scroll-overflow-x findings are collapsed to the deepest flagged element that explains the overflow (each scroll-container-leak-x finding stays independent because it models a distinct parent/child boundary); pass lint_options: { collapse_cascade: false } to keep the full raw cascade.

Browser tools and coding agents

The IIFE is useful for browser tools that can load an init script:

agent-browser --session ui \
  --init-script ./node_modules/overflowlint/dist/overflowlint.core.js \
  open http://localhost:5173
agent-browser --session ui set viewport 390 844
agent-browser --session ui --json eval \
  "OverflowLint.run({ report_to_console: false })"

For a version-pinned browser ESM import:

<script type="module">
  import { OverflowLint } from 'https://cdn.jsdelivr.net/npm/overflowlint@0.5.0/+esm'
  OverflowLint.watch()
</script>

Privacy and security

Scans execute in the inspected page. OverflowLint has no telemetry and does not transmit page data. Panel clipboard writes only happen after a user presses a copy button.

Bookmarklets and userscripts execute broadly inside visited pages and can read those pages. Install them only from a source you trust. Prefer the npm package and Playwright injection for controlled development and CI environments.

Limitations

OverflowLint is a development aid, not a replacement for visual review, accessibility testing, responsive product judgment, or cross-browser testing. It does not traverse iframes, infer whether every intentional overlap is good, or prove that a clean layout is attractive. Geometry can change after a scan, and closed shadow roots remain opaque.

Use representative viewports and realistic content. Render, scan, fix, and rerun in the same state.

Distribution

Stable bookmarklets and userscripts load the latest published npm release from jsDelivr. The project website may describe unreleased main behavior. Dev channels track main and belong in advanced testing only. Pin npm versions for repeatable automation.

Development

Requires the pinned Node and pnpm versions.

pnpm dev
pnpm format
pnpm format:check
pnpm test:unit
pnpm test:browser
pnpm test:browser:all
pnpm test:performance
pnpm test:docs
pnpm test:package
pnpm verify

pnpm test is the fast coding loop: unit tests plus Chromium E2E, excluding the performance suite. pnpm test:browser:all runs the functional suite in Chromium, Firefox, and WebKit. Performance budgets run separately and serially with pnpm test:performance to avoid cross-browser CPU contention.

pnpm verify is the definition of done. It checks formatting, types, unit tests, all three browser engines, documentation, build artifacts, and a fresh packed consumer. When the adjacent Eva repository exists locally, verification also runs its typecheck.

License

MIT

Generated rule and contract reference

This file is generated from the runtime registries. Run pnpm generate:reference after changing either registry.

Rules

Rule ID Default severity Category Level Origin Description
page-horizontal-overflow error layout essential inferred The rendered document is wider than the viewport.
page-vertical-overflow error layout essential contract The document is taller than a body-level no-scroll contract permits.
scroll-container-leak-x warning layout recommended inferred A horizontal scroll container escapes its parent boundary.
inferred-scroll-overflow-x warning layout recommended inferred Content overflows horizontally without a containing scroll boundary.
clipped-content-overflow-x warning visibility recommended inferred Content is masked by a horizontal clipping boundary.
clipped-content-overflow-y warning visibility recommended inferred Content is masked by a vertical clipping boundary.
interactive-covered error interaction essential inferred Browser hit testing finds another element covering an interactive target.
small-interactive-target warning interaction recommended inferred An interactive target is smaller than its minimum target size.
text-truncation warning visibility recommended inferred Rendered text is cut off by ellipsis, nowrap clipping, or line clamping.
data-ol-no-scroll-x error layout essential contract An element violates an explicit horizontal no-scroll contract.
data-ol-no-scroll-y error layout essential contract An element violates an explicit vertical no-scroll contract.
data-ol-in-viewport error visibility essential contract An element required in the viewport is not visibly rendered there.
data-ol-min-visible error visibility essential contract An element does not meet its minimum visible-area ratio.
data-ol-contained-by error layout essential contract An element escapes its asserted containing boundary.
data-ol-no-overlap error layout essential contract An element overlaps a selector it is required not to overlap.
interactive-zero-area error interaction experimental inferred A semantically enabled interactive element has no usable rendered area.
interactive-clipped warning interaction experimental inferred An interactive element is substantially hidden by a clipping boundary.
interactive-transparent warning interaction experimental inferred A focusable or hit-testable interactive element is fully transparent.
interactive-pointer-disabled warning interaction experimental inferred A semantically enabled interactive element disables pointer interaction.
interactive-offscreen warning visibility experimental inferred A fixed or sticky interactive element is wholly outside the viewport.
element-horizontal-viewport-escape warning layout experimental inferred Meaningful content escapes the viewport without creating document scroll.
flex-item-overflow warning layout experimental inferred A direct flex item escapes its container content boundary.
grid-item-overflow warning layout experimental inferred A direct grid item escapes its container content boundary.
sibling-content-overlap warning layout experimental inferred Rendered content belonging to sibling regions overlaps.
fixed-sticky-obstruction warning visibility experimental inferred Fixed or sticky content substantially obstructs another content region.
excessive-dom-size info performance experimental inferred The rendered body contains more than 1,400 elements.
excessive-dom-depth info performance experimental inferred The rendered DOM tree exceeds 32 element levels.
excessive-dom-children info performance experimental inferred A rendered element has more than 60 direct element children.
excessive-z-index-complexity info performance experimental inferred The rendered page uses many explicit or distinct z-index levels.

Contracts and allowances

Attribute Purpose
data-ol-no-scroll Assert no scroll overflow.
data-ol-allow-scroll Permit intentional scroll overflow on this element and axis.
data-ol-allow-clip Permit intentional clipping on this element and axis.
data-ol-in-viewport Assert that an element is visible in the viewport.
data-ol-clickable Mark a custom element as interactive.
data-ol-min-target Set an interactive target minimum in CSS pixels.
data-ol-name Provide a friendly stable selector name.
data-ol-ignore Skip an element, its descendants, and its hit-test occlusion.
data-ol-no-truncate Suppress text-truncation detection for this element.
data-ol-min-visible Assert a viewport-visible area ratio from 0 to 1.
data-ol-contained-by Assert containment by parent, viewport, or an ancestor selector.
data-ol-no-overlap Assert no overlap with elements matching a selector.
data-ol-overlay-chrome Mark persistent app chrome or an overlay whose hit-test occlusion is intentional.
data-ol-off-canvas Mark an element intentionally positioned off-canvas (e.g., a hidden drawer).

Result protocol

Every LintResult has schema_version: 2. The JSON schema ships at dist/schema.json.

The singleton surface is limited to run, stress_text, watch, stop, watching, version, and schema_version.