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

on_the_money

v0.3.3

Published

Opinionated, attribute-driven, standards-oriented modern framework. <2KB gzip. Native browser APIs only.

Readme

on_the_money.js

Opinionated, attribute-driven, standards-oriented modern framework for the web. <2KB gzip. Native browser APIs only. ESNext.

State lives in DOM attributes. Reactivity comes from [data-text] and [data-i18n] selectors. Events use one delegated listener per (parent, type) pair. Routing uses the History API. Localization uses Intl. There is no virtual DOM, no JSX, no transpilation step, no proprietary tooling. The framework is a thin layer of conventions over the platform.

Install

npm install on_the_money

Quickstart

A complete two-file app. index.html carries semantic structure, app.js carries behavior.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title data-i18n="app_title">My App</title>
  <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2/css/pico.classless.min.css">
  <script type="module" src="./app.js"></script>
</head>
<body>
  <main>
    <h1 data-i18n="app_title">My App</h1>
    <p>Hello, <strong data-text="user">friend</strong>.</p>
    <button data-action="greet">Greet</button>
  </main>
</body>
</html>
// app.js
import { the, on } from "on_the_money";

await the.boot({
  dictionary: { app_title: "On The Money" },
});

on("main", "click", '[data-action="greet"]', () => {
  the("user", "Alice");
});

The framework rehydrates body data-* and [data-text] from localStorage on boot; subsequent the() writes both the attribute and persist to localStorage["otm:KEY"].

Pico Classless integration

Pico Classless is the recommended companion stylesheet. It styles semantic HTML directly — no classes, no JS. Include the stylesheet once:

<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2/css/pico.classless.min.css">

Then write semantic HTML (<main>, <nav>, <article>, <form>, <button>, <input> with <label>, etc.). on_the_money's conventions all use data-* and aria-* attributes, so they layer cleanly under Pico's element-targeted CSS.

Exports

import { on, the, _t, route, $, $$ } from "on_the_money";

Aliases on the: the.t === _t, the.route === route, the.form(formEl), the.flat(obj, sep?), the.boot(options?). Live accessors on the: the.dictionary, the.locale.

API

on(parent, event, selector, fn) — event delegation

| Arg | Type | Notes | | --- | --- | --- | | parent | Element \| string \| null | If string, resolved via document.querySelector. If falsy, defaults to document.body. | | event | string | Any DOM event name ("click", "submit", "mounted", etc.). | | selector | string | CSS selector matched via event.target.closest(selector). | | fn | (event, target) => void | target is the element the selector matched. |

Returns an () => void unsubscribe function.

const off = on("#list", "click", "[data-action='delete']", (e, target) => {
  target.closest("[data-item]").remove();
});
off(); // detaches the listener

on.emit(el, event, detail)CustomEvent dispatch

Dispatches a bubbling, cancelable CustomEvent on el. el may be an element or a selector string.

on.emit("#cart", "items-changed", { count: 3 });

the(...) — state

Polymorphic on a single disambiguator: args[0] instanceof Element. Three call shapes per scope (get, set, batch).

| Call | Behavior | Returns | | --- | --- | --- | | the(key) | Read body data-KEY (or aria-mapped equivalent). | string \| null | | the(key, val) | Write body attribute + localStorage["otm:KEY"] + descendant [data-text="key"]. | document.body | | the({ k: v, ... }) | Batch global write. | document.body | | the(el, key) | Read scoped attribute on el. | string \| null | | the(el, key, val) | Write scoped attribute on el + descendant [data-text="key"]. | el | | the(el, { k: v, ... }) | Batch scoped write. | el |

  • ARIA mapping (key → attribute): expanded, selected, hidden, checked, disabledaria-*. All other keys → data-*.
  • Booleans coerce to "true"/"false" inside the setter. the(el, "checked", true) writes "true".
  • Values MUST be flat primitives. Pass nested objects through the.flat(...) first.
  • the(key, undefined) throws. Two args means set; missing val is a contract violation.

the.form(formEl) — form extraction

Walks input, select, textarea descendants. Skips unnamed, disabled, submit/button/reset controls, and unchecked checkboxes/radios. Parses bracket-notation names into a nested object:

the.form(form);
// <input name="user[name]" value="Alice">     → { user: { name: "Alice" } }
// <input name="tags[]" value="a">             → { tags: ["a", ...] }
// <input name="tags[]" value="b">

Returns a nested object (matches browser submission semantics). Compose with the.flat before feeding to the(el, {...}).

the.flat(obj, sep = "_") — nested-to-flat

the.flat({ user: { name: "Alice" }, tags: ["a", "b"] });
// → { user_name: "Alice", tags_0: "a", tags_1: "b" }

the.flat({ a: { b: 1 } }, ".");
// → { "a.b": 1 }

Throws on non-object input.

the.boot(options?) — explicit init

Importing the module does nothing. Call the.boot() at the consumer's entry point.

await the.boot({ signal, locales, dictionary, namespace, defaultLocale });

| Option | Type | Behavior | | --- | --- | --- | | signal | AbortSignal | Aborts the i18n fetch. | | locales | string | Override the <meta name="i18n" content> path. | | dictionary | object | Inline dictionary; skips the fetch entirely. | | namespace | string | Sets localStorage prefix to ${namespace}: (default otm:). Must be set before any state ops. | | defaultLocale | string | Locale your static HTML is already written in. When the resolved locale base matches this, the dictionary fetch and _t() hydration pass are skipped entirely — no network, no FOUC. Auto-detected from <html lang> if omitted. | | ephemeralKeys | string[] | Global state keys that should not persist to localStorage. Writes to these keys still update the body attribute and any [data-text] mirrors, but skip the storage write; the boot replay also skips them. Use for transient signals like modal, loading, toast. Scoped state (the(el, ...)) never persists regardless. |

Boot sequence:

  1. Resolve locale: ?lang= query → localStorage["${prefix}lang"]navigator.language. Writes the.locale.
  2. Short-circuit check. If resolved locale base matches defaultLocale (or <html lang> if not provided), skip steps 3 and 5. Static HTML already serves the right text.
  3. Resolve dictionary: inline dictionaryfetch(${path}/${target}.json) if <meta name="i18n"> or locales option is present. Falls back through full → base → data-fallback.
  4. Replay localStorage entries matching the prefix (except ${prefix}lang) back onto body data-* and [data-text].
  5. Run _t() to hydrate [data-i18n].

Writing data-i18n elements: always include the source-language text inside as a fallback:

<!-- good — SEO fallback, no-JS fallback, no-flash default -->
<h1 data-i18n="title">Welcome to the app</h1>

<!-- avoid — page is blank until hydration -->
<h1 data-i18n="title"></h1>

The framework preserves existing textContent when a dictionary key is missing, so source-language text inside data-i18n elements stays visible if _t() runs against an empty dictionary (the short-circuit case above, or any environment without the i18n fetch).

_t(key, options)Intl localization

_t("hello");                                       // string lookup in the.dictionary
_t("hello", { name: "Alice" });                    // {name} → "Alice"
_t("items", { qty: 5 });                           // Intl.PluralRules picks { one, other } entry
_t("price", { val: 9.99, type: "currency" });      // Intl.NumberFormat (USD)
_t("when", { val: Date.now(), type: "date" });     // Intl.DateTimeFormat

_t(node);                                          // hydrate every [data-i18n] inside node
_t();                                              // hydrate document.body

Missing dictionary keys preserve existing textContent (SEO fallback). When options.type is "currency" or "date", Intl formatting of options.val runs regardless of whether the dictionary has an entry — useful in default-locale apps that skip the fetch.

[data-i18n] element binding (always include source-language fallback text inside):

<span data-i18n="cart_items" data-i18n-qty="3">3 items</span>
<span data-i18n="price" data-i18n-val="9.99" data-i18n-type="currency">$9.99</span>

route(callback) — pushState router

route((pathname, search, hash) => {
  // render whatever fits this route
});

Fires the callback on initial mount, on popstate, on hashchange, and on intercepted internal <a> clicks. Skips interception for data-external, target="_blank", and cross-origin hrefs. Hash-only same-page links are left to the browser; hashchange still triggers the callback.

$(context, selector) / $$(context, selector) — context-aware DOM query

$(el, ".child");        // first match (Element | null)
$$(el, ".child");       // all matches (Array)
$(".child");            // shorthand: context = document
$$(".child");

$.clone(parent, selector) — template instantiation

const el = $.clone("#list", "#tmp");

Clones the first element of <template selector>, runs _t(el) for i18n hydration, appends to parent, dispatches a bubbling mounted CustomEvent (detail: { parent }), and returns the mounted element. Throws if parent or template is missing.

Patterns

Form intake

on("#todo-form", "submit", (e) => {
  e.preventDefault();
  const data = the.form(e.target);                          // { task, tags: [...] }
  the($.clone("#todo-list", "#todo-item"), the.flat(data)); // task, tags_0, tags_1 → attrs
});

List rendering with i18n

<template id="post-card">
  <article data-item>
    <h2 data-text="title"></h2>
    <p data-i18n="posted_at" data-i18n-val=""></p>
  </article>
</template>
<section id="posts"></section>
for (const post of posts) {
  const el = $.clone("#posts", "#post-card");
  the(el, { title: post.title });
  $(el, '[data-i18n="posted_at"]').setAttribute("data-i18n-val", post.created_at);
}
_t();

Routing (multi-page SPA)

route((path) => {
  the("page", path === "/" ? "home" : path.slice(1));
});
main > section { display: none; }
[data-page="home"]    main > section[data-route="home"]    { display: block; }
[data-page="about"]   main > section[data-route="about"]   { display: block; }
[data-page="contact"] main > section[data-route="contact"] { display: block; }

Hot dictionary swap

on("nav", "click", "[data-lang]", async (_e, link) => {
  const lang = link.getAttribute("data-lang");
  the.locale = lang;
  the.dictionary = await (await fetch(`/locales/${lang}.json`)).json();
  _t();
});

Namespaced state

// Two apps on the same origin can coexist:
await the.boot({ namespace: "dashboard" }); // localStorage keys: dashboard:theme, etc.

Cleanup with on() unsubscribe

const off = on("#modal", "click", "[data-action='close']", closeModal);
on.emit(document.body, "modal-closed");
off(); // detach when modal is destroyed

Server-side rendering (no extra API)

There's no SSR shim. Emit data-* attributes from the server:

<!-- rendered server-side -->
<body data-theme="dark" data-user="Alice">
  <h1 data-text="user">Alice</h1>
</body>

On the client:

await the.boot();   // localStorage values, if any, override the server-rendered attrs

The framework reads existing attributes via the(key) without modification, so server-rendered state is observable immediately. the.boot() only mutates when localStorage has matching keys.

Lint stack

on_the_money ships a three-tool stack. Each layer covers what the others can't.

1. JavaScript — ESLint + bundled plugin

The ESLint plugin and config ship inside on_the_money itself. No separate eslint-plugin-otm package — import via subpath.

npm install -D eslint eslint-plugin-no-unsanitized
// eslint.config.js
import otm from "on_the_money/eslint-config";
import nounsanitized from "eslint-plugin-no-unsanitized";

export default [
  ...otm,
  nounsanitized.configs.recommended,
];

| Rule | Source | Behavior | | --- | --- | --- | | otm/prefer-on | bundled in on_the_money | Ban addEventListener; use on(). | | otm/prefer-the-set | bundled in on_the_money | Ban textContent/innerText/nodeValue assignment. | | otm/flat-state | bundled in on_the_money | Ban nested objects/arrays in the() calls. | | otm/prefer-submit | bundled in on_the_money | Warn on on(btn, "click", ...) for form data. | | otm/no-style-mutation | bundled in on_the_money | Ban el.style.* = .... | | no-unsanitized/no-inner-html | eslint-plugin-no-unsanitized | Ban innerHTML/outerHTML. | | no-unsanitized/method | eslint-plugin-no-unsanitized | Ban document.write, insertAdjacentHTML. |

2. CSS — Stylelint + bundled plugin

Same pattern: the Stylelint plugin and config ship inside on_the_money. No separate stylelint-plugin-otm package.

npm install -D stylelint stylelint-config-standard
// stylelint.config.js
import otm from "on_the_money/stylelint-config";

export default {
  extends: ["stylelint-config-standard"],
  ...otm,
};

| Rule | Source | Behavior | | --- | --- | --- | | otm/prefer-attribute-selector | bundled in on_the_money | Ban .class selectors; use [data-state="..."]. | | declaration-no-important | stylelint built-in | Ban !important. |

3. HTML / cross-file — otm-lint

npx otm-lint --check ./src

| Rule | Forbidden | Use instead | | --- | --- | --- | | HTML-004 | Naked text in HTML | Wrap in a semantic tag or use data-i18n="key". | | HTML-017 | <div data-action="..."> without role/tabindex | Use a <button> or other interactive element. | | HTML-023 | data-i18n="..." without <meta name="i18n"> | Declare the i18n endpoint. | | HTML-024 | data-available="..." doesn't match locales folder | Keep the manifest aligned with the actual locale files. |

otm-lint default-excludes node_modules, dist, .git, and dotdirs. It only scans .html — everything else delegates to the layers above.

Recommended companions (not shipped)

| Concern | Tool | | --- | --- | | HTML correctness (lang/charset/viewport, inline handlers, deprecated attrs) | html-validate | | Static a11y | eslint-plugin-jsx-a11y (works on plain HTML via parsers) | | Runtime a11y | axe-core via Playwright/Cypress against the rendered page | | Supply-chain | npm audit, osv-scanner |

Suggested project layout

my-app/
├── index.html              # Pico + meta tags + script entry
├── app.js                  # await the.boot(); register handlers
├── locales/
│   ├── en.json
│   ├── es.json
│   └── fr.json
├── styles.css              # attribute-selector-driven custom CSS (optional)
└── views/
    ├── home.html           # fragment imported via fetch + $.clone, or inline <template>
    └── about.html

<meta name="i18n" content="/locales" data-available="en,es,fr" data-fallback="en"> directs the.boot() to fetch /locales/{lang}.json automatically.

Repository layout (contributors)

src/
├── core/
│   ├── index.js           # public exports
│   ├── On.js              # event delegation + emit
│   ├── The.js             # state, i18n, boot, route, form, flat
│   └── Select.js          # $, $$, clone
└── linter/
    ├── main.js            # otm-lint binary entrypoint
    ├── cli.js             # directory scan, output
    └── Linter.js          # rule implementations
test/
└── integration.test.js    # cross-module integration suite
dist/
└── on_the_money.min.js    # built ESM bundle (esbuild)

Unit tests are co-located with source: src/core/On.test.js, etc.

Commands

| Script | Purpose | | --- | --- | | npm test | Run all tests (node --test). | | npm run test:coverage | Tests + coverage report (50/50/50 line/branch/function gate). | | npm run lint | Biome formatter + import order. | | npm run build | Build dist/on_the_money.min.js via esbuild. | | npm run check | lint + build + test + coverage. CI gate. | | npm run otm | Self-lint examples/ against project rules. |

License

MIT. Source and issues: https://github.com/possumtech/on_the_money.js