npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

boxy-layout

v0.1.0

Published

Spatial layout linter for Playwright with verified CSS causation analysis

Readme

Boxy

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

Not pixel diffing. Boxy builds a spatial model of your rendered page and detects broken layout patterns. When something breaks, it tells you which CSS property changed on which element caused it — verified by re-rendering with each change in isolation.

Live demo & docs →

The Problem

The worst UI bugs are the ones where:

  • Someone adds overflow: hidden to a container and now dropdowns are clipped
  • A modal gets overflow: hidden and select menus inside it are cut off
  • A sidebar collapses to 0px and all nav items disappear
  • An element gets visibility: hidden or opacity: 0 and nobody notices
  • A z-index change buries a popover behind another element

These bugs are invisible to pixel diffing because they only appear after specific interactions (open menu, scroll, open another menu), depend on content length or viewport size, and the baseline screenshot was taken in a different state.

Install

npm install -D boxy-layout

Quick Start

Add capture() calls to your existing Playwright tests after interactions:

import { createBoxy } from 'boxy-layout';

const boxy = createBoxy();

test('users page', async ({ page }) => {
  await page.goto('/users');
  await boxy.capture(page, { name: 'users-loaded' });

  // Open a dropdown — does it get clipped?
  await page.click('[data-testid="filter-status"]');
  await boxy.capture(page, { name: 'filter-open' });

  // Open action menu on last row — does it overflow the table?
  await page.click('[data-testid="action-btn"]');
  await boxy.capture(page, {
    name: 'action-menu',
    scope: '[data-testid="users-table"]',
  });
});

afterAll(() => {
  const exitCode = boxy.report();
  boxy.writeHTMLReport();
  if (exitCode) throw new Error('boxy found layout errors — see .boxy/report.html');
});

Throw rather than process.exit(exitCode): calling process.exit inside afterAll truncates Playwright's own reporting and teardown, so you lose the test report. Throwing fails the suite cleanly and lets Playwright finish.

Parallel workers

Under parallel Playwright workers (--workers=N), Boxy writes a per-worker HTML report (report-w<idx>.html) so workers don't overwrite each other, and baseline/screenshot writes are atomic with a first-writer-wins auto-save. Give every capture() a name that is unique across the whole suite — workers share one baseline directory, so a duplicate name from two workers compares against the same baseline.

How It Works

First run (no baseline)

Baselines are auto-saved on first run, but missing baselines fail by default so CI catches an absent trust anchor. For an intentional setup run:

LAYOUT_INIT=true npx playwright test

On the first run, Boxy saves the current layout as the baseline and runs lint checks only (no regression comparison). Without LAYOUT_INIT=true or acceptNewBaselines: true, that baseline-created notice is reported as a failure.

Subsequent runs

On subsequent runs, Boxy compares against the saved baseline and reports both lint issues and regressions.

Updating baselines after intentional changes

When you intentionally change the layout, update baselines:

LAYOUT_UPDATE=true npx playwright test

Or per-capture:

await boxy.capture(page, { name: 'redesigned-header', update: true });

Resetting baselines

boxy.resetBaseline('dashboard');   // delete one baseline
boxy.resetAllBaselines();          // clear all baselines and current snapshots

Lint checks (no baseline needed)

| Check | What it catches | |-------|----------------| | Clipping | Element extends beyond parent with overflow: hidden/auto/scroll | | Overlap | Positioned elements overlapping where high z-index is clipped | | Collapsed | Element has near-zero width/height but contains content | | Off-screen | Positioned element outside viewport bounds |

Regression checks (with baseline)

| Check | What it catches | |-------|----------------| | Spacing | Gap between siblings changed beyond threshold | | Position | Element shifted significantly from baseline | | Size | Element width/height changed >30% | | Visibility | Element disappeared, became visibility:hidden, or opacity:0 | | CSS diff | Shows which computed styles changed on which elements | | Viewport guard | Viewport or scroll position differs from baseline — comparison is skipped with a single clear error instead of a wall of bogus position diffs |

Verified Causation Analysis

When a regression is detected, Boxy can prove exactly which CSS property caused it. With the page still in its broken state, each candidate change is reverted to its baseline value in isolation and the layout is re-measured: if reverting a property moves the layout back toward the baseline, it's a verified root cause; if nothing changes, it had no impact.

// After capturing a regression — keep the page in the same (broken) state
const step = await boxy.capture(page, { name: 'detail', scope: '[data-testid="panel"]' });
const result = await boxy.diagnoseCauses(page, 'detail');
┌─────────────────────────────────────────────────────────┐
│              Causation Analysis                         │
└─────────────────────────────────────────────────────────┘
  8 re-renders performed

  ┌ ROOT CAUSES (1 verified)
  │ [data-testid="detail-body"]
  │   maxHeight: none → 150px
  │     → shifted 39 elements, resized 2 elements
  └

  ┌ NO IMPACT (6 verified)
  │ [data-testid="detail-header"]
  │   zIndex: auto → 99
  │ [data-testid="detail-close"]
  │   maxWidth: none → 9999px
  │ [data-testid="detail-body"]
  │   overflow: auto → hidden
  │ [data-testid="detail-body"]
  │   overflowX: auto → hidden
  │ [data-testid="detail-body"]
  │   overflowY: auto → hidden
  └

7 CSS properties changed. Only 1 actually broke the layout. Boxy proves it.

Configuration

const boxy = createBoxy({
  snapshotDir: '.boxy',              // where to store baselines
  update: false,                     // set true to overwrite baselines (or use LAYOUT_UPDATE=true)
  allowMissingBaseline: true,        // auto-save baseline on first run (set false to require existing baseline)
  acceptNewBaselines: false,         // set true for intentional setup runs (or use LAYOUT_INIT=true)
  stabilize: true,                   // wait for fonts + layout to settle before each capture
  config: {
    spacingThreshold: 4,     // px — spacing changes below this are ignored
    positionThreshold: 20,   // px — position shifts below this are ignored
    sizeChangePercent: 30,   // % — size changes below this are ignored
    collapsedMinSize: 5,     // px — elements smaller than this are flagged
    ignore: [                // selectors to skip
      '.scrollable-list',
      '[data-testid="carousel"]',
    ],
  },
});

Output

Terminal

┌─────────────────────────────────────────────────────────┐
│              Layout Lint Results                        │
└─────────────────────────────────────────────────────────┘

  ✓ users-loaded
  ✓ filter-open
  ✗ action-menu (4 errors, 0 warnings)

    ┌ CLIPPING
    │ ✗ Element clipped by parent overflow
    │   [data-testid="action-menu"]
    │     clipped by: [data-testid="users-table"]
    │     hidden: bottom: 94px
    └

    ┌ CSS CHANGES (caused layout impact)
    │ [data-testid="users-table"]
    │   overflow: visible → hidden
    └

HTML Report

Generated at .boxy/report.html with screenshots and issue details.

CI

Exit code 1 when errors are found:

- run: npx playwright test

Testing

# Smoke test (good version should pass clean)
npm test

# Broken version should catch clipping errors
npm run test:broken

# Interaction-driven scenario tests
npm run test:scenarios
npm run test:scenarios:broken

# Mutation suite: inject 9 realistic CSS bugs, verify all are caught
npm run test:mutations

Current mutation detection rate: 9/9 (100%)

| Mutation | What it simulates | |---|---| | table-overflow-hidden | overflow:hidden added to scrollable table container | | modal-overflow-hidden | Modal clips select dropdowns inside forms | | sidebar-collapse-to-zero | Collapsed sidebar width set to 0 instead of icon-width | | detail-panel-overflow-hidden | Detail panel body loses scroll, clips long content | | filter-popover-under-table | Filter popover z-index lowered, goes behind sticky header | | notif-dropdown-off-screen | Notification dropdown positioned with wrong offset | | row-dropdown-opens-down | Action menu opens downward instead of upward on last rows | | nav-items-visibility-hidden | Nav items set to visibility:hidden | | pagination-opacity-zero | Pagination faded to opacity:0 |

How It Works

  1. Captures the bounding box, computed styles, z-index, overflow, visibility, opacity, and sibling spacing for every visible element — after waiting for fonts to load and layout to stop changing, so animations and late font swaps can't poison the model
  2. Lints the spatial model for broken patterns (clipping, overlap, collapse)
  3. Compares against baseline (if available) for regressions (spacing, position, size, visibility)
  4. Diffs computed CSS between baseline and current, classifying changes as effective (caused spatial impact) or inert (no layout difference)
  5. Verifies causation (optional) to identify which specific CSS property caused the breakage

Why Not Percy / Chromatic?

| | Pixel diffing | Boxy | |---|---|---| | False positives | High (anti-aliasing, fonts, rendering) | Low (spatial analysis) | | Explains the bug | "These pixels changed" | "Dropdown clipped because overflow: auto → hidden" | | Identifies root cause | No | Yes — pinpoints the CSS property that caused it | | Needs cloud infra | Yes | No (runs locally) | | Needs baseline | Always | Only for regression checks | | Post-interaction testing | Only if you screenshot every state | Captures spatial model after any interaction |

License

MIT