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

@a11y-pulse/text-spacing-audit

v0.1.0

Published

WCAG 1.4.12 Text Spacing audit that injects the SC spacing overrides and detects clipped or overlapping text.

Readme

@a11y-pulse/text-spacing-audit

npm version CI License: PolyForm Shield 1.0.0

An accessibility audit for WCAG 1.4.12 Text Spacing. It injects the success criterion's spacing overrides (the same stylesheet used by Steve Faulkner's text spacing bookmarklet), measures candidate text containers, and reports text that is newly clipped or newly overlapping. It is framework-agnostic and can run in any environment that can evaluate JavaScript in a page, such as Puppeteer, Playwright, or Selenium.

This audit was developed by A11y Pulse for its accessibility monitoring service. It is released as source-available under the PolyForm Shield License 1.0.0.

Install

npm install @a11y-pulse/text-spacing-audit puppeteer

puppeteer is an optional peer dependency. It is only required if you use the bundled Puppeteer adaptor. Other frameworks can supply their own adaptor without installing Puppeteer at all.

Quickstart

import { runTextSpacingAudit } from "@a11y-pulse/text-spacing-audit";
import { PuppeteerAdaptor } from "@a11y-pulse/text-spacing-audit/puppeteer";
import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://who.likesdogs.nz/");

const result = await runTextSpacingAudit(new PuppeteerAdaptor(page));

console.log(result.summary);
// { clipped: 0, truncationIncreased: 0, overlaps: 0 }

console.log(result.findings);
// []

await browser.close();

See examples/puppeteer for a complete, runnable example.

What it checks

WCAG 1.4.12 requires that content and functionality survive when a user sets:

  • line height to at least 1.5 times the font size
  • spacing following paragraphs to at least 2 times the font size
  • letter spacing to at least 0.12 times the font size
  • word spacing to at least 0.16 times the font size

The audit does not require authors to use those values. It checks that the page still shows its text when they are applied.

Static tools such as axe-core's avoid-inline-spacing only flag inline !important spacing declarations that would block a user override. This audit applies the override and looks at the outcome. Leave axe's rule enabled: the two checks are complementary, and inline !important spacing is left to axe because a stylesheet !important cannot override it.

Bookmarklet methodology

The procedure matches the bookmarklet used by the WCAG community:

  1. Wait for document.fonts.ready and freeze CSS animations/transitions so motion does not look like clipping.
  2. Collect visible text containers (elements with a non-whitespace direct text node), capped at candidateLimit.
  3. Inject:
* { line-height: 1.5 !important; letter-spacing: 0.12em !important; word-spacing: 0.16em !important; }
p { margin-bottom: 2em !important; }
  1. Re-measure the same elements and classify:
    • clipped (the only violation class): overflow hidden/clip, content fitted at baseline, then exceeded the clip box by more than clipTolerancePx.
    • truncation-increased (incomplete): already truncated (ellipsis / line-clamp) and overflow grew.
    • overlap (incomplete): nearby text rects that did not intersect at baseline and now do. Sticky and fixed elements are skipped.
  2. Remove the injected styles and verify a sample of rects match the baseline. A mismatch sets restored: false but never throws.

Growth and reflow without clipping are not findings. That is the correct response to a spacing override.

Options

The following options can be passed to runTextSpacingAudit as TextSpacingOptions:

| Option | Type | Default | Description | | ------------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------- | | candidateLimit | number | 500 | Max visible text containers to measure, in document order. | | clipTolerancePx | number | 2 | Overflow in px that must be exceeded before a clip or truncation increase counts. | | settleMs | number | 200 | Extra wait after two animation frames for layout to settle after injecting or removing styles. |

Result shape

runTextSpacingAudit resolves to a TextSpacingResult:

type TextSpacingResult = {
  findings: Array<{
    selector: string;
    html: string;
    kind: "clipped" | "truncation-increased" | "overlap";
    metrics: { beforeOverflowPx: number; afterOverflowPx: number };
    overlapsWith?: string;
  }>;
  candidateCount: number;
  restored: boolean;
  summary: {
    clipped: number;
    truncationIncreased: number;
    overlaps: number;
  };
};

candidateCount is the number of stable (non-moving) text containers that were measured. clipped findings are the confident loss-of-content signal. truncation-increased and overlap are heuristic and should be treated as incomplete, not violations.

Adaptors

The audit itself is framework-agnostic: it drives a page through an adaptor, a small interface of primitives that the audit calls without knowing which browser automation library is behind it.

The package ships one implementation, PuppeteerAdaptor, backed by a Puppeteer Page. Other environments (Playwright, Selenium, WebDriver) can be supported by implementing the same interface, exported as TextSpacingAuditAdaptor (aliased as BrowserAdaptor from the package root).

TextSpacingAuditAdaptor / BrowserAdaptor

| Method | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------- | | evaluate(fn, ...args) | Runs fn in the page context, passing in any serialisable args, and returns its result. |

Writing a new adaptor

Implement TextSpacingAuditAdaptor from @a11y-pulse/text-spacing-audit (or its BrowserAdaptor alias) against your automation library's page/session object, then pass an instance to runTextSpacingAudit:

import type { TextSpacingAuditAdaptor } from "@a11y-pulse/text-spacing-audit";

class MyFrameworkAdaptor implements TextSpacingAuditAdaptor {
  // ...implement evaluate for your framework
}

Use src/adaptors/puppeteer.ts as a reference implementation. It is a thin page.evaluate wrapper.

Limitations

  • Overlap is incomplete, never a violation. Overlap detection is heuristic (nearby pairs only; sticky/fixed skipped). Decorative overlaps and stacking contexts can still produce noise.
  • Inline !important spacing is left to axe. A user stylesheet cannot beat an inline !important declaration. axe-core avoid-inline-spacing covers that blocker; this audit cannot restyle those elements.
  • Already-clipped content is not attributed to the override. Only new overflow inside a clipping box is reported as clipped.
  • Intentional truncation. Single-line ellipsis and line-clamp that were already truncating route to truncation-increased (incomplete), not clipped.
  • Motion. CSS animations are frozen before the baseline. Elements whose rects move between two samples (JS-driven motion) are excluded.
  • Main frame only. Text in canvas, images, or cross-origin iframes is invisible to the audit. Closed shadow roots are opaque; open shadow roots are walked.
  • Paragraph spacing is p only. That matches the bookmarklet. Pages that use divs as paragraphs are under-tested.
  • Loss of functionality without a geometric symptom (for example a click target covered by a transparent sibling) is not detected.
  • Forcing line-height: 1.5 can reduce spacing on a page that already exceeds the minimum. That is faithful to the bookmarklet. Clipping under 1.5 still implies clipping under anything larger.

Releasing

Releases are managed in the A11y-Pulse/audits monorepo with Changesets. Publishing uses npm trusted publishing (OIDC). There is no long-lived NPM_TOKEN.

Ship a change

  1. Open a PR against main that includes a changeset (npx changeset) naming @a11y-pulse/text-spacing-audit.
  2. After merge, the Release workflow opens a Version PR. Merging that PR publishes this package to npm and tags @a11y-pulse/text-spacing-audit@<version>.

Trusted Publisher on npm must stay configured for:

| Field | Value | | --- | --- | | Organization or user | A11y-Pulse | | Repository | audits | | Workflow filename | release.yml |

Consumers (e.g. the A11y Pulse runner)

Bumping the published version in downstream apps is a separate change. Update the dependency range / lockfile there after the npm release lands.

License

Released under the PolyForm Shield License 1.0.0, in plain language:

  • Source-available. The source is public and you can read, fork, and modify it.
  • Permitted for non-competing use. You can use this package freely in your own products and services, as long as they don't compete with A11y Pulse.
  • Competing products are forbidden. You may not use this software (or a modified version of it) to build a product or service that competes with A11y Pulse's accessibility monitoring offering.

See LICENSE.md for the full, binding terms.