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

@platformdesign/components

v0.3.3

Published

Native web components. No build step, no framework, no dependencies — just the browser.

Readme

@platformdesign/components

Native web components. No build step, no framework, no dependencies.

npm install @platformdesign/components
<script type="module">
    import '@platformdesign/components/pl-button';
</script>

<pl-button data-variant="primary">Save</pl-button>

That's the whole integration. The library ships standard ES modules and standard CSS, so a <script type="module"> tag is a complete install — there is nothing to compile, configure, or keep current.


Why

Every component library eventually asks you to adopt its ecosystem. This one asks you to use the browser's.

  • No toolchain. Ship the source. It's ES modules and CSS.
  • No framework. A custom element is an HTML element — it works in React, Vue, Angular, Svelte, Rails, Django, or a static file.
  • No theming API. Custom properties are the only thing that crosses a shadow boundary, so they are the theming API. Override a token; every component follows.
  • No supply chain. Zero dependencies means zero transitive dependencies. Nothing to audit, nothing to hijack upstream, no postinstall scripts.

Usage

Import one component — this registers <pl-button> and nothing else:

import '@platformdesign/components/pl-button';

Or register everything at once:

import '@platformdesign/components';

Classes are exported too, for subclassing or instanceof:

import { Button } from '@platformdesign/components';

The source path resolves as well, if you prefer knowing where a file lives:

import '@platformdesign/components/app/inputs/pl-button';

The optimised build

Every import above resolves to source. That is the default and it stays the default: the files you import are the files that run, and if you have a bundler it is also the best thing to give it — yours minifies and tree-shakes across your whole app in ways a pre-bundled file cannot.

For a page with no build step, the source costs requests. The package therefore also ships a built copy under the /min subpath, opt-in by import path:

// the whole library, one request
import '@platformdesign/components/min';

// or just what you use — they share chunks, so the second costs almost nothing
import '@platformdesign/components/min/pl-button';
import '@platformdesign/components/min/pl-hero';
<script type="module" src="/node_modules/@platformdesign/components/dist/platform.js"></script>

Measured in brotli, which is what a CDN actually sends:

| What you load | As source | Built | Saving | | --- | --- | --- | --- | | One component | 7 files, 13.9 kB | 4 files, 6.2 kB | 55% | | Two components | 11 files, 15.4 kB | 7 files, 6.8 kB | 56% | | A 13-tag landing page | 37 files, 38.6 kB | 20 files, 23.2 kB | 40% | | The whole library | 125 files, 85.8 kB | 1 file, 43.9 kB | 49% |

min/tokens.css and min/global.css are the minified stylesheets; global.css is bundled, so the starter is one request instead of an @import chain.

It is a subpath rather than a browser export condition on purpose. A condition would hand a bundler minified input behind its back, and the whole point of source being the default is that nothing swaps it out without you asking.

Compression is the server's job — none of those numbers happen without brotli or gzip enabled.

Shipping both copies roughly doubles the tarball, to about 1 MB unpacked across 201 files. That is a deliberate trade and an affordable one: it is install weight, never runtime weight, since no page loads both halves. It also still lands well under a single framework's core — and with zero dependencies, the install stops there rather than pulling a tree behind it.

Styles

Components carry their own styles. The one stylesheet you load is the token file — the design tokens every component reads, and the entire theming surface.

@import "@platformdesign/components/tokens.css";

It's optional: every component references tokens with fallbacks and renders correctly without it. Loading it (or your own theme) is what makes the set share one system.

Theming

Platform Components is the sibling of platformdesign.app. You design a system there, export it as CSS custom properties, and drop it in — the components read those exact token names, so an export is drop-in.

The contract is a naming convention: one prefix per type, no project namespace, so the tokens a designer produces and the tokens a component reads are the same tokens.

:root {
    --color-primary: #2563EB;      /* solid fill for controls */
    --color-on-primary: #FFFFFF;   /* text carried on that fill */
    --color-surface: #FFFFFF;
    --color-ink: #111827;
    --size-16: 1rem;
    --border-radius-medium: 8px;
}

The package's tokens.css ships a deliberately neutral default — a conventional blue primary with green, amber, and red intents — so it reads as a starting point rather than as somebody else's brand. Point the contract tokens at your own values to re-theme everything.

A filled control always pairs an intent fill with its on-color, and every on-color is white, in light and dark alike. Dark text on a saturated fill is the usual way a button breaks when a theme flips, so that pairing is fixed rather than derived from the page's ink.

A theme is a single palette. There is no light-dark() in the tokens; "dark mode" is a different export swapped in (e.g. by toggling data-theme and re-pointing the semantic --color-* tokens). Components re-theme instantly because they read the names, not a scheme.

Your tokens vs the components' tokens

Components never read the contract names directly. They read a parallel set of --pl-* aliases, and tokens.css points each one at its contract counterpart:

:root {
    --color-primary: #2563EB;                 /* the contract */
    --pl-color-primary: var(--color-primary); /* what components read */
}

That one level of indirection gives you both halves of what you usually have to choose between:

  • Inheritance — the alias resolves lazily, so whatever --color-primary computes to on your page is what components use. Load order doesn't matter.
  • Insulation — the alias is a seam. If your application wants its own --color-primary for its own layout, distinct from the primary its components render with, pin --pl-color-primary instead and the two can diverge.

Keep tokens.css loaded — it is the bridge. Components fall back to built-in defaults when an alias is missing, so an export loaded on its own would be silently ignored.

For a one-off, each component also exposes --<component>-* hooks (--button-background, --button-color) that sit in front of the tokens.


Repository layout

Library/                    the published package
  _core/
    elements/               base classes (BaseElement, ButtonElement, …)
    utilities/              createNativeElement, htmlElementSpec, props
    styles/                 tokens.css and shared style modules
  components/
    app/                    interactive UI — mostly Shadow DOM
      inputs/ ui/ surfaces/ navigation/ state/ media/
    content/                page content — Light DOM
      sections/ structure/ pages/
  utilities/                framework-free helpers

dist/                       generated by `npm run build` — publishes, but is
                            only reachable through the /min subpath
public/                     the documentation site
Developer_Docs/             authoring guide and architecture principles
scripts/                    dev server and metadata generation

Shadow vs Light

The split that matters most:

  • components/app/ is interactive UI and uses Shadow DOM for style encapsulation.
  • components/content/ is page content and uses Light DOM, so it stays visible to the page's cascade, to search crawlers, and to browser translation.

Some app components are Light DOM too, when their whole purpose is a document-level relationship a shadow boundary would break — <pl-label> is the clearest case, since <label> association is scoped to a single DOM tree.


Development

npm run dev        # serve the docs site at localhost:3000
npm test           # run the test suite
npm run build      # regenerate dist/ (the optional /min distribution)
npm run exports    # regenerate package exports, barrel, and docs nav
npm run release -- patch   # bump 0.x.y → 0.x.y+1 and publish
npm run release -- minor   # bump 0.x.y → 0.x+1.0 and publish

Releases use semver. While the package is pre-1.0, stay on 0.y.zmajor is blocked unless you pass --confirm-v1. Add --dry-run to preview, --no-publish to bump package.json only, or --otp=xxxxxx when account 2FA is enabled.

dist/ is in files, so whatever is on disk is what publishes. The release script refuses to run when dist/ is older than Library/: a stale build under a fresh version number is worse than no build, because /min consumers would silently get the previous release's code. Run npm run build first.

The tests need a DOM. Since the package itself ships zero dependencies, jsdom isn't one either — point at a copy you already have, or the suite skips rather than fails:

JSDOM=/path/to/node_modules/jsdom/lib/api.js npm test

npm run build resolves esbuild the same way, for the same reason — the zero-dependency promise covers what consumers install, and it would be hollow if the repo quietly grew a toolchain:

ESBUILD=/path/to/node_modules/esbuild/lib/main.js npm run build
npm run build -- --report    # print the size table, write nothing

npm run exports reads Library/components/ and rewrites three generated files: the exports map in package.json, the Library/index.mjs barrel, and public/js/nav.data.mjs. Run it after adding or removing a component directory — never edit those three by hand.

The dev server serves Library/ directly, so the documentation imports the same source a consumer gets from npm — never dist/. The build is an addition, not a step: nothing in the repo depends on it having been run, and the docs cannot document a stale copy.


Authoring a component

Declare typed props once; observedAttributes derives from them.

import { BaseElement, define } from '@platformdesign/components/_core/elements/BaseElement.mjs';

export class Example extends BaseElement {
    static props = {
        open: { type: Boolean, default: false },
    };

    render() {
        this.refs.panel.hidden = !this.props.open;
    }
}

define('pl-example', Example);

Values are typed and coerced — this.props.open is a real boolean, and assigning a bad value throws. See Developer_Docs/component-authoring-guide.md for the full model, including the Shadow/Light decision and how reflection avoids feedback loops.


Browser support

Requires ES modules, custom elements, and CSSStyleSheet.replaceSync — Chrome/Edge 120+, Safari 16.4+, Firefox 115+. No polyfills are shipped, and none are planned; the whole point is to use what the browser already does.


License

MIT