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
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.