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

@spec-ui/core

v0.0.1

Published

SPEC — Spatial Properties & Element Contracts. Executable specs for design systems.

Readme

SPEC

Spatial Properties & Element Contracts

Your nav looks fine at 1200px. But at 375px, the links overlap the hamburger menu. Nobody catches it until a user files a bug.

SPEC catches it before they do.

spec<Breakpoints, Components>({
  component: "Nav",
  breakpoints: {
    1200: async (t) => {
      await t.assert(tid("nav-links"), isVisible());
      await t.assert(tid("hamburger"), isHidden());
      await t.assert(tid("nav-logo"), isLeftOf(tid("nav-links")));
    },
    768: skip,
    375: async (t) => {
      await t.assert(tid("nav-links"), isHidden());
      await t.assert(tid("hamburger"), isVisible());
      await t.assert(tid("nav-logo"), doesNotOverlap(tid("hamburger")));
    },
  },
});

SPEC is an executable specification framework for design systems. Define contracts for your components — dimensions, CSS properties, spatial relationships — and verify them in a real browser at every breakpoint. TypeScript ensures you never forget a viewport size.

This is not a regression tool ("did it change?"). It's a specification tool ("is it correct?").

Install

npm install -D @spec/core

Quick start

npx spec init       # scaffold config + example spec
npx spec test       # run all specs

Setup

Create spec.config.ts (or run npx spec init):

import { defineConfig } from "@spec/core";
import type { BreakpointsOf, ComponentsOf } from "@spec/core";

const config = defineConfig({
  baseURL: "http://localhost:6006",
  tolerance: 2,
  components: {
    "Button/primary": "/?path=/story/button--primary",
    "Nav": "/?path=/story/nav--default",
  },
  breakpoints: [1200, 768, 375],
});

export default config;
export type Breakpoints = BreakpointsOf<typeof config>;
export type Components = ComponentsOf<typeof config>;

Point each component at any URL that renders it — Storybook, a sandbox page, a static HTML file. SPEC doesn't care how you serve your components.

Tag elements with data-tid attributes:

<nav data-tid="nav">
  <span data-tid="nav-logo">Logo</span>
  <div data-tid="nav-links">...</div>
  <button data-tid="hamburger">☰</button>
</nav>

Write specs

The opinionated pattern: define your contracts at the top, assert against them in the spec. A Contract defines what a component's CSS must satisfy — it's not duplicating CSS, it's the independent verification that CSS is correct.

import { spec, skip, tid, all, hasWidth, hasHeight, hasCSS, whenHovered } from "@spec/core";
import type { Contract } from "@spec/core";
import type { Breakpoints, Components } from "../spec.config";

// --- Contracts ---

const btnPrimary: Contract = {
  backgroundColor: "rgb(59, 130, 246)",
  borderRadius: "6px",
  color: "rgb(255, 255, 255)",
};

const btnPrimaryHover: Contract = {
  backgroundColor: "rgb(37, 99, 235)",
};

// --- Spec ---

spec<Breakpoints, Components>({
  component: "Button/primary",
  breakpoints: {
    1200: async (t) => {
      await t.assert(tid("btn-primary"), all(
        hasWidth(200),
        hasHeight(48),
        hasCSS(btnPrimary),
      ));

      await t.assert(tid("btn-primary"), whenHovered(hasCSS(btnPrimaryHover)));
    },
    768: skip,
    375: async (t) => {
      await t.assert(tid("btn-primary"), hasHeight(40));
    },
  },
});

Contract is powered by csstype — the same type system behind React's CSSProperties. Full autocomplete for every CSS property.

Breakpoint enforcement

spec<Breakpoints>() requires every breakpoint from your config. Missing one is a compile error. Use skip for breakpoints that don't need assertions — it's explicit: "I've considered this viewport and there's nothing to verify."

Stability

Elements with CSS transitions or animations may not have their final styles immediately. Use stable() to wait until an element stops moving:

await t.stable(tid("animated-panel"));
await t.assert(tid("animated-panel"), hasCSS(panelOpen));

Run

npx spec test
SPEC running 2 spec file(s)

  Button/primary
  ✓ Button/primary @ 1200px (84ms)
  ⊘ Button/primary @ 768px (skipped)
  ✓ Button/primary @ 375px (42ms)

  Nav
  ✓ Nav @ 1200px (54ms)
  ⊘ Nav @ 768px (skipped)
  ✓ Nav @ 375px (56ms)

  4 passed, 2 skipped
  total: 1.24s

Snapshot

Don't want to write specs from scratch? Snapshot the current state of your components and use the output as a starting point:

npx spec snapshot                    # all components
npx spec snapshot --filter Button    # just one
SPEC snapshot capturing 1 component(s)

  capturing Button/primary → http://localhost:6006/?path=/story/button--primary
    ✓ wrote button-primary.spec.ts

Snapshot complete

Review the generated specs, adjust contracts and tolerances, then run:
  npx spec test

The generated file captures every data-tid element — dimensions, computed CSS, and spatial relationships at each breakpoint:

import { spec, skip, tid, all, hasWidth, hasHeight, hasCSS } from "@spec/core";
import type { Contract } from "@spec/core";

// --- Contracts ---

const btn_primary: Contract = {
  backgroundColor: "rgb(59, 130, 246)",
  color: "rgb(255, 255, 255)",
  fontSize: "16px",
  borderRadius: "6px",
  padding: "12px 24px",
  cursor: "pointer",
};

// --- Spec ---

spec<1200 | 768 | 375>({
  component: "Button/primary",
  breakpoints: {
    1200: async (t) => {
      await t.assert(tid("btn-primary"), all(
        hasWidth(200),
        hasHeight(48),
        hasCSS(btn_primary),
      ));
    },
    768: async (t) => {
      await t.assert(tid("btn-primary"), all(
        hasWidth(200),
        hasHeight(48),
        hasCSS(btn_primary),
      ));
    },
    375: async (t) => {
      await t.assert(tid("btn-primary"), all(
        hasWidth(295),
        hasHeight(40),
        hasCSS(btn_primary),
      ));
    },
  },
});

Review the output, trim it to what you actually want to enforce, add your Breakpoints and Components types, and you're done. The snapshot does the measuring — you decide what matters.

Matchers

All matchers are pure functions. Import only what you need.

Value

const input: Contract = { borderRadius: "6px", fontSize: "16px" };

await t.assert(tid("btn"), hasWidth(200));
await t.assert(tid("btn"), hasHeight(48));
await t.assert(tid("btn"), hasWidth(200, { tolerance: 5 }));
await t.assert(tid("input"), hasCSS(input));

Spatial

await t.assert(tid("nav"), isAbove(tid("content")));
await t.assert(tid("sidebar"), isLeftOf(tid("main")));
await t.assert(tid("logo"), isInside(tid("nav")));
await t.assert(tid("card-a"), doesNotOverlap(tid("card-b")));
await t.assert(tid("tooltip"), overlaps(tid("trigger")));
await t.assert(tid("label"), hasGap(tid("input"), 8));

Alignment and centering

await t.assert(tid("label"), isAlignedWith(tid("input"), "top"));
await t.assert(tid("modal"), isCenteredIn(tid("overlay")));
await t.assert(tid("title"), isCenteredHorizontallyIn(tid("hero")));
await t.assert(tid("sidebar"), isFlushWith(tid("content"), "top"));

Relative sizing

await t.assert(tid("container"), isWiderThan(tid("card")));
await t.assert(tid("icon"), isShorterThan(tid("label")));
await t.assert(tid("btn-a"), isSameSizeAs(tid("btn-b")));

Visibility and stacking

await t.assert(tid("nav-links"), isVisible());
await t.assert(tid("hamburger"), isHidden());
await t.assert(tid("modal"), isStackedAbove(tid("backdrop")));

Combinators

all — multiple assertions on one element

await t.assert(tid("btn"), all(
  hasWidth(200),
  hasHeight(48),
  hasCSS(btnPrimary),
));

not — negate any matcher

await t.assert(tid("modal"), not(isHidden()));

nth — target the Nth match

await t.assert(tid("list-item"), nth(2, hasCSS(activeItem)));

State combinators

Assert against interaction states:

const btnHover: Contract = { backgroundColor: "rgb(37, 99, 235)" };
const inputFocused: Contract = { borderColor: "rgb(59, 130, 246)" };

await t.assert(tid("btn"), whenHovered(hasCSS(btnHover)));
await t.assert(tid("input"), whenFocused(hasCSS(inputFocused)));
await t.assert(tid("checkbox"), whenChecked(hasCSS(checked)));
await t.assert(tid("email"), whenValid(hasCSS(inputFocused)));

Media combinators

const darkBg: Contract = { backgroundColor: "rgb(15, 23, 42)" };

await t.assert(tid("body"), whenDark(hasCSS(darkBg)));
await t.assert(tid("animation"), whenReducedMotion(hasCSS({ animationDuration: "0s" })));

Custom matchers

A matcher is a function: (selector, ctx) => Promise<{ pass, message }>. Write your own:

import type { Matcher } from "@spec/core";

const hasMinWidth = (min: number): Matcher =>
  async (selector, ctx) => {
    const box = await ctx.page.locator(selector).boundingBox();
    if (!box) return { pass: false, message: "element not found" };
    return {
      pass: box.width >= min,
      message: box.width >= min
        ? `width ${box.width}px >= ${min}px`
        : `expected min width ${min}px, got ${box.width}px`,
    };
  };

await t.assert(tid("container"), hasMinWidth(300));

Custom matchers compose with all, not, whenHovered, and all other combinators.

CLI

Commands

npx spec test              # run all specs
npx spec list              # list registered specs and URLs
npx spec init              # scaffold config and specs directory

Test options

npx spec test button             # file pattern match
npx spec test --filter Nav       # component name filter
npx spec test --workers 4        # parallel browser tabs
npx spec test --watch            # re-run on file changes
npx spec test --bail             # stop on first failure
npx spec test --headless false   # show browser (debugging)
npx spec test --json             # JSON output (CI)
npx spec test --slow 200         # slow test threshold (default: 500ms)

Options compose:

npx spec test --watch --workers 4 --filter Button

Config auto-discovery

SPEC looks for spec.config.ts, .js, or .mjs automatically. Use --config to override.

JSON output

{
  "results": [
    { "component": "Nav", "breakpoint": 1200, "status": "pass", "duration": 54 },
    { "component": "Nav", "breakpoint": 375, "status": "fail", "duration": 62,
      "error": "elements overlap", "selector": "[data-tid=\"nav-logo\"]" }
  ],
  "passed": 1,
  "failed": 1,
  "skipped": 0,
  "duration": 1042
}

How it works

  1. Loads config, discovers *.spec.ts files, validates components
  2. For each spec + breakpoint: opens a browser tab, sets viewport, navigates to the component URL
  3. Runs assertions against real getBoundingClientRect() and getComputedStyle() values
  4. Reports results grouped by component with failure details, slow warnings, and timing

No JSDOM. No fake DOM. Real browser, real rendering.

License

MIT