Drop-in layout linter for Playwright. Add a few lines to your existing tests to catch clipping, overflow, and CSS regressions automatically.

$ npm install boxy-layout click to copy

Your CSS change broke 8 dropdowns.

Boxy tells you which property caused it — before your users do.
No new tests to write. It rides on the Playwright tests you already have.

Try It Live

Toggle CSS mutations on a real UI, then run Boxy's analysis engine in your browser.

Mutations

Sample UI (1440 x 900)

Results

Toggle a mutation, then click Run Boxy

How It Works

1

Capture

Boxy walks the DOM and builds a spatial model: bounding boxes, computed styles, overflow chains, sibling spacing, z-index stacking.

2

Lint

The linter detects issues without a baseline: clipping by overflow parents, collapsed containers, off-screen elements, z-index conflicts.

3

Compare

With a baseline, regression analysis catches position shifts, size changes, visibility loss, and spacing inconsistencies — with CSS diff attribution.

Why Boxy?

Pixel diffing tells you something changed. Boxy tells you what broke and why.

Pixel diff

Fragile
  • Fails on font rendering, antialiasing, animation timing
  • Shows a red blob — you still have to find the bug
  • No idea which CSS property caused the change
  • Needs exact screenshot match across environments
  • Useless to AI agents — an LLM can't debug a pixel diff

Manual QA

Slow
  • Can't test every page state after every CSS change
  • Misses edge cases: last row dropdown, collapsed sidebar, deep scroll
  • Doesn't scale with component count
  • Catches issues after deploy, not during CI

Usage

1 Install
$ npm install boxy-layout
2 Add to an existing Playwright test
import { test } from '@playwright/test'; import { createBoxy } from 'boxy-layout'; test('layout has no regressions', async ({ page }) => { const boxy = createBoxy(); await page.goto('http://localhost:3000/dashboard'); // Capture the spatial model — bounding boxes, styles, overflow chains await boxy.capture(page, { name: 'dashboard', scope: '[data-testid="app"]' }); // Open a dropdown and capture again await page.click('[data-testid="action-btn"]'); await boxy.capture(page, { name: 'action-menu-open', scope: '[data-testid="app"]' }); // Print issues and fail if any errors boxy.report(); expect(boxy.hasErrors()).toBe(false); });
3 Run tests (baselines auto-saved on first run)
$ npx playwright test # First run: auto-saves baselines to .boxy/baseline/ # Subsequent runs: compares against saved baselines
4 Update baselines after intentional changes
$ LAYOUT_UPDATE=true npx playwright test # Overwrites baselines, then reports: CLIPPING Element clipped by parent overflow [data-testid="action-dropdown"] clipped by: [data-testid="table-area"] hidden: bottom 136px, left 134px POSITION Element position shifted [data-testid="sidebar-nav"] delta: x+220px CSS CHANGES: overflow: visible → hidden (effective) width: 220px → 0px (effective)