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

pixelpact-core

v0.4.1

Published

Extract a visual contract from a reference page and measure an implementation against it

Readme

pixelpact-core

Measure what a reference page actually looks like, store it as a contract, then hold an implementation to that contract.

Most visual testing compares a screenshot against an older screenshot of the same thing. pixelpact compares an implementation against a reference: the design you are rebuilding, the site you are migrating, the page your new framework has to reproduce. The contract is measured, not written by hand, so "it looks right" becomes a number.

Install

npm install pixelpact-core
npm install playwright        # peer dependency
npx playwright install chromium

playwright is a peer dependency and is imported lazily. Reading, validating and formatting contracts works without a browser installed.

Quick start

import { check, extract, formatCheckReport, writeContract } from 'pixelpact-core'

const contract = await extract({
  url: 'https://reference.example',
  selector: 'main',
  screenshotDir: './pixelpact',
})
await writeContract('./pixelpact/contract.json', contract)

const report = await check(contract, { url: 'http://localhost:3000' })
process.stdout.write(formatCheckReport(report, { color: true }))
process.exitCode = report.ok ? 0 : 1

What is measured

For every visible element under the root selector, at every viewport:

  • the bounding box, in CSS pixels relative to the document
  • around seventy computed properties: colour, typography, spacing, borders, radii, shadows, layout, transforms, transitions and animations
  • the hover and focus states, by actually hovering and focusing the element and recording only the declarations that changed
  • the ::before and ::after pseudo elements when they render content
  • the CSS custom properties declared on :root, and every @keyframes rule
  • a screenshot per viewport, when screenshotDir is set

Figma as a source

A contract can also come from a Figma file, read over the REST API. No browser is launched and nothing is rendered locally.

import { extractFromFigma, check, writeContract } from 'pixelpact-core'

const contract = await extractFromFigma({
  url: 'https://www.figma.com/design/<file-key>/Site?node-id=1-23',
  token: process.env.FIGMA_TOKEN,   // this is the default
  screenshotDir: './pixelpact',
})
await writeContract('./pixelpact/figma.contract.json', contract)

const report = await check(contract, { url: 'http://localhost:3000' })

The token is a Figma personal access token with file content read access, created under Settings, Security, Personal access tokens. extractFromFigma reads FIGMA_TOKEN unless you pass one.

Matching is different, and this is the whole story

A Figma layer has no CSS selector, so a Figma contract cannot be matched by path or by tag name. A layer type says nothing about which HTML element implements it, so pixelpact does not guess. Matching runs on the layer name.

Give the layer a name in Figma, then put the same name in the markup:

<a class="btn btn-primary" data-contract="Hero/CTA">Start free</a>

Every layer without a matching data-contract is reported as missing rather than skipped. When a check finds no match at all, the report prints the reason and what to do about it; figmaMatchHint(report) returns the same sentence for a tool that formats its own output.

What a layer contributes

| Figma | Contract | | --- | --- | | layer name | contractId, the only thing matching uses | | node id | selector, written figma:1:23 as a display label | | node type | tag, lowercased: frame, text, instance | | absoluteBoundingBox | box, translated so the root frame sits at 0, 0 | | solid fill | background-color, or color on a text layer | | stroke and strokeWeight | the four border widths, border-top-style, border-top-color | | cornerRadius, rectangleCornerRadii | the four radius longhands | | drop shadow | box-shadow | | opacity below 1 | opacity | | text style | font family, size, weight, line height, letter spacing, alignment, transform | | auto layout | display: flex, direction, gap, the four paddings, align-items, justify-content | | colour styles | tokens, keyed by the style name |

Only properties the layer actually carries are written. A gradient or image fill has no single CSS colour, so the property is left out and the layer is named in contract.warnings. A value that is not in the contract is never asserted, which is the point: an empty assertion is honest, a guessed one fails an implementation that is correct.

Hidden layers, and everything under them, are skipped. Hover, focus and pseudo element states are absent, because Figma has none to read, so a check against a Figma contract never reports them.

Side by side, section by section

check returns properties and diff returns one percentage for a whole page. Neither says where the page is wrong. side splits both pages into sections, pairs them by position, and writes one image per pair: the reference on the left, the implementation on the right, both at the same scale, the differing pixels boxed in red, and a header carrying the section, the width and the percentage.

import { formatSideReport, side } from 'pixelpact-core'

const report = await side({
  referenceUrl: 'https://reference.example',
  targetUrl: 'http://localhost:3000',
  outDir: './pixelpact/side',
  widths: [1440, 390],
})

process.stdout.write(formatSideReport(report, { color: true }))
process.exitCode = report.ok ? 0 : 1
pixelpact side  FAILED
  reference https://reference.example
  target    http://localhost:3000
  widths    1440px, 390px
  sections  5 passed, 1 failed of 6 (threshold 0.5%)

#   SECTION  WIDTH   VERDICT  DIFF
01  hero     1440px  PASS     0.000%
02  pricing  1440px  FAIL     1.193%
  ./pixelpact/side/1440/02-pricing.png
03  contact  1440px  PASS     0.000%

Sections are the visible element children of sectionsSelector, or of main when the page has one and body otherwise. Each one is named after its id, then its first heading, then its position, and that name is both the slug in the report and the file name of the image.

Pairing is by position. When the two pages disagree on how many sections they have, the surplus is counted in unmatched and named in warnings rather than paired with a guess: a report that invented a counterpart would show a picture of one section under the name of another. ok is true when every compared section is inside threshold and nothing was left unmatched.

Run one section at a time with only, by index or by part of its slug:

await side({ referenceUrl, targetUrl, outDir: './side', only: 'pricing' })

API

| Function | What it does | | --- | --- | | extract(options) | Measure a reference page and return a Contract | | extractFromFigma(options) | Read a Figma file over the REST API and return a Contract | | parseFigmaUrl(input) | Read the file key and node id out of a figma.com url | | isFigmaUrl(input) | True when a string is a figma.com url with a file key | | figmaMatchHint(report) | The data-contract advice, when a Figma check matched nothing | | check(contract, options) | Compare a live implementation against the contract | | diff(contract, options) | Compare the implementation to the reference screenshot, pixel by pixel | | side(options) | Compare two live pages section by section and compose one image per section | | parseContract(input) | Validate unknown data as a contract | | readContract(path) | Read and validate a contract from disk | | writeContract(path, contract) | Validate and write a contract as JSON | | formatCheckReport(report, options) | Render a check report as an aligned table | | formatDiffReport(report, options) | Render a diff report as a few lines | | formatSideReport(report, options) | Render a side by side report as one row per section | | DEFAULT_VIEWPORTS | desktop 1440x900, tablet 768x1024, mobile 390x844 |

Options

Every browser backed entry point accepts the browser options: headless, channel, executablePath, locale, timezone, userAgent, stealth, wait, dismissOverlays and timeout. Defaults are portable: locale en-US, time zone UTC, and the browser's own user agent.

extract adds selector, viewports, maxElements, maxStates, masks, freezeAnimations, fullPage and screenshotDir. check adds viewport, selector, tolerance and maxStates. diff adds viewport, selector, threshold, masks and outDir. side takes referenceUrl, targetUrl and outDir instead of url, and adds widths, sectionsSelector, only, threshold, minSectionHeight, columnWidth and masks. Its outDir has no default: the composed images are the product of the run, so the caller says where they go.

extractFromFigma launches no browser, so none of the browser options apply to it. It takes url, token, nodeId, viewportName, maxElements, screenshotDir, scale and onProgress.

Configuration is arguments only. Nothing is read from a config file, from the current working directory or from the environment, with two exceptions: executablePath falls back to PIXELPACT_CHROMIUM, so a machine that already has a browser does not have to download another, and the Figma token falls back to FIGMA_TOKEN, so a secret never has to be written into a script.

check and diff inherit locale, timezone, wait, stealth, dismissOverlays and userAgent from the contract unless you pass your own. Those settings decide what a page renders, and measuring the implementation under different ones produces failures that have nothing to do with its CSS. Machine settings such as headless, channel and timeout are never inherited.

Progress

The library never prints. Pass onProgress to follow a long run:

await extract({
  url: 'https://reference.example',
  onProgress: (event) => process.stderr.write(`${event.phase}: ${event.message}\n`),
})

Reading a check report

pixelpact check  FAILED
  target    http://localhost:3000
  reference https://reference.example
  viewport  desktop 1440x900
  elements  128 matched, 2 missing of 130
  checks    2841 passed, 19 failed (99.3% of 2860)

deviations (19)
SELECTOR                    PROPERTY     EXPECTED           ACTUAL             DIFF
main > section > h2         font-size    32px               28px               4px
nav > a:nth-of-type(2)      hover.color  rgb(0, 90, 200)    rgb(0, 0, 0)       76.5 (color)

formatCheckReport takes color (default false, ANSI codes emitted by the library itself) and limit (default 20 rows, then a line counting the rest).

How elements are matched

An implementation rarely has the same DOM as its reference, so matching runs in three passes, most trustworthy first:

  1. data-contract="..." on both sides. Add the attribute where you want a guaranteed match.
  2. An identical CSS path.
  3. The same tag carrying the same text.

An element that answers to none of the three is reported as missing. A Figma contract uses the first pass only, for the reason given above.

Tolerances

  • Lengths pass within tolerance pixels, default 1.
  • Box dimensions also pass within one percent of the expected size: a pixel on a 24px button matters, a pixel on a 1440px hero is rounding.
  • Colours are compared by perceived distance, not by string. A difference above roughly 2 is visible side by side and is reported. A difference in alpha is always reported.
  • width and height are compared from the bounding box only, so a mismatch is never reported twice.
  • The transform of an element that animates forever is skipped: it is a reading of a moment, not a promise.

Pixel diffing

diff needs a reference screenshot, so the contract must have been extracted with screenshotDir. If the contract declares a root selector, that selector has to exist in the implementation too: comparing a whole page against a shot of one section produces a number that measures nothing, so it fails instead.

Output goes to outDir, which defaults to the system temp directory.

Errors

Every error thrown on purpose extends PixelpactError and carries a code.

| Class | Code | When | | --- | --- | --- | | ContractError | ERR_CONTRACT | The contract is missing, unreadable, invalid, or lacks what a step needs | | BrowserUnavailableError | ERR_BROWSER | playwright is not installed, or no browser could be launched | | BlockedPageError | ERR_BLOCKED | The page answered with a bot challenge and nothing could be measured | | TargetNotFoundError | ERR_TARGET | A url would not load, or a selector matched nothing | | FigmaError | ERR_FIGMA | The Figma url, token, file key or node id is wrong, or the API refused |

Determinism

Two runs of the same page should produce the same numbers, so the browser is launched with a fixed sRGB colour profile and hinting disabled, the locale and time zone are always set, animations are frozen before a screenshot, and the page is scrolled and decoded before it is captured. Hover states are read after the element's own transition has finished rather than after a fixed delay, which is what keeps a 250ms fade from being sampled mid flight.

License

MIT