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

minu

v1.6.1

Published

Lightweight HTML fragment loader — AJAX, polling, and live SSE content with zero dependencies

Readme

Minu

Project page · npm · Examples

Lightweight HTML fragment loader. Fetch and inject server-rendered HTML — with AJAX, forms, polling, out-of-band swaps, and live SSE content. Zero runtime dependencies, ~4.0 KB gzipped. MIT licensed; contributions welcome.

Minu is for server-rendered apps that want htmx-style behavior without the framework. Your server keeps returning HTML; Minu puts it where it belongs.

Quick Start

CDN:

<script src="https://unpkg.com/minu/dist/minu.min.js"></script>
<script>
    minu.init();
</script>

npm:

npm install minu
const minu = require('minu');   // CommonJS
// or: import minu from 'minu'; // ESM
minu.init();

Usage

Add an attribute with the URL you want to fetch:

<div minu-get="/widgets/price" minu-trigger="load" minu-target="#price">
    <div minu-loader>Loading…</div>
</div>
<div id="price">$0.00</div>

Minu fetches the URL, sanitizes the HTML, and swaps it into the target. With no explicit target it uses a minu-content child, then the single minu-content on the page, then the element itself.

Attributes

Requests

| Attribute | Description | |---|---| | minu-get | Fetch with GET | | minu-post | Submit with POST | | minu-put / minu-patch / minu-delete | Other verbs | | minu-trigger | Event or trigger expression (default load) | | minu-target | CSS selector for the swap target (this allowed) | | minu-swap | innerHTML (default), outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, none | | minu-vals | JSON object of extra values | | minu-include | Selector(s) of extra fields/forms to serialize | | minu-headers | JSON object of request headers | | minu-credentials | omit, same-origin, include | | minu-timeout | Timeout in ms | | minu-confirm | confirm() message before sending | | minu-prevent | Set to false to allow native submit/click behavior | | minu-select | Extract this CSS selector from the response before swapping |

Forms are serialized automatically when the trigger is inside a form (or is the form). Files produce multipart/form-data; everything else is form-encoded. GET values are appended to the query string.

UI state

| Attribute | Description | |---|---| | minu-loader | Element shown during the request (legacy; display toggled) | | minu-indicator | Selector(s) receiving the minu-requesting class | | minu-disable | Selector(s) disabled during the request (self/this allowed) | | minu-content | Default target inside the element |

Prefer minu-indicator + CSS. It keeps inline/flex layouts intact:

.spinner { display: none; }
.minu-requesting { display: inline-block; }

Triggers

minu-trigger accepts a DOM event name plus whitespace-separated modifiers:

| Trigger | Meaning | |---|---| | load | On init (default) | | click, submit, change, keyup, … | Any DOM event | | every 5s | Poll; skips while the tab is hidden | | revealed | Load once when the element enters the viewport | | intersect | Load every time the element enters the viewport | | once | Fire at most once | | changed | Only when the field value changed | | delay:300ms | Debounce | | throttle:500ms | Throttle | | from:#other | Listen on another element |

<input minu-get="/search" minu-trigger="keyup changed delay:300ms"
       minu-target="#results">
<div id="results"></div>

Submit and click events are prevented by default when Minu handles them. Forms need no preventDefault() workaround.

Events

All events bubble and carry detail.

| Event | Detail | |---|---| | minu:contentSetup | { url, trigger } | | minu:beforeRequest | { url, method, element } — cancelable | | minu:contentLoaded | { html, url, method } | | minu:contentError | { error, url } | | minu:afterRequest | { url, method, ok } | | minu:sseOpen | { url } | | minu:sseMessage | { data, event, url } | | minu:sseError | { error, url } | | minu:trigger:<name> | { name, url } — from Minu-Trigger header |

document.addEventListener('minu:contentLoaded', (event) => {
    console.log('loaded', event.detail.url);
});

document.getElementById('save').addEventListener('minu:beforeRequest', (event) => {
    if (!confirm('Save?')) event.preventDefault();
});

API

| Method | Description | |---|---| | minu.init(selector?) | Initialize matching elements (idempotent) | | minu.reloadComponents(selector \| selector[]) | Re-fetch matching elements | | minu.reloadComponent(...) | Deprecated alias | | minu.config({ headers, timeout, credentials }) | Global request defaults | | minu.destroy() | Abort requests, remove listeners, close streams | | minu.closeStream(element?) | Close one or all SSE streams |

minu.config({
    headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content },
    timeout: 10000,
});

Server Protocol

Response headers:

| Header | Effect | |---|---| | Minu-Trigger: refresh | Dispatches minu:trigger:refresh on the requestor (comma-separated allowed) | | Minu-Redirect: /login | Navigates | | Minu-Refresh: true | Reloads the page |

Out-of-band swaps update other regions from one response:

<!-- response -->
<div id="cart" minu-oob="#cart">3 items</div>
<div id="toast" minu-oob="beforeend:#toasts">Saved</div>

minu-oob defaults to outerHTML on the selector. An OOB-only response leaves the main target untouched.

Real-time Content (SSE)

Minu supports server-sent events with the same target/swap/OOB pipeline.

<div minu-sse="/events/prices"
     minu-sse-event="price"
     minu-target="#price"
     minu-swap="innerHTML"></div>

<div id="price">$0.00</div>
<div id="ticker">waiting…</div>

The server sends HTML fragments:

event: price
data: <span class="price">$42.00</span><div id="ticker" minu-oob="#ticker">42 ms</div>

Rules:

  • EventSource reconnects automatically; retry: and id: (Last-Event-ID) work as usual.
  • minu-sse-event is comma-separated; default message.
  • Every message is sanitized and swapped through minu-swap.
  • minu.destroy() closes streams; minu.closeStream(el) closes one.
  • Cross-origin streams need CORS. Disable proxy buffering (X-Accel-Buffering: no).

SSE is the recommended live-update path. Chat sends use normal minu-post; only the receive side is a stream. WebSocket support is intentionally not included — add it only if you need bidirectional push.

JSON

JSON is supported for attribute configuration only:

  • minu-vals — JSON object merged into request values. Nested values are stringified.
  • minu-headers — JSON object merged into request headers.

Minu does not send JSON request bodies and does not parse JSON responses. Non-GET bodies are application/x-www-form-urlencoded or multipart/form-data; every response is read as text and treated as an HTML fragment.

For a JSON API, render HTML on the server from the JSON, or use fetch and your own renderer. See the runnable example/json/ page in the repository for the exact boundary.

Security

Do not open a public issue for a security vulnerability. Email [email protected] first, with the subject “Minu security report”. See SECURITY.md for scope and disclosure expectations.

minu.js strips scripts, inline event handlers, and javascript: URLs. It is not an XSS boundary — regex sanitization never is.

For untrusted HTML, install DOMPurify and use the secure build:

npm install minu dompurify
import minu from 'minu/secure';
minu.init();

The secure build throws at load if DOMPurify is missing.

Examples

Run these from a repository checkout — the published npm package ships only the built library and typings.

npm install
npm run build
npm run examples   # http://localhost:3000

| Example | Live page | Shows | |---|---|---| | Basic | https://aotr.github.io/minu/example/basic/ | Auto-load a widget | | Form | https://aotr.github.io/minu/example/form/ | POST, loader, indicator, disable, confirm | | Tabs | https://aotr.github.io/minu/example/tabs/ | Click-triggered shared target | | Search | https://aotr.github.io/minu/example/search/ | Debounce, changed, minu-select | | Polling | https://aotr.github.io/minu/example/polling/ | every 2s and visibility pause | | Lazy reveal | https://aotr.github.io/minu/example/lazy/ | revealed / intersect | | Swap modes | https://aotr.github.io/minu/example/swap/ | All eight swap behaviors | | OOB | https://aotr.github.io/minu/example/oob/ | One response updates three regions | | Headers | https://aotr.github.io/minu/example/headers/ | Minu-Trigger, Minu-Redirect, Minu-Refresh | | Events | https://aotr.github.io/minu/example/events/ | Lifecycle log and cancellation | | JSON | https://aotr.github.io/minu/example/json/ | JSON attributes vs. JSON responses | | Live SSE | https://aotr.github.io/minu/example/live/ | SSE price + OOB ticker |

The GitHub Pages site is static, so its dynamic demos use a small browser-side adapter that simulates the server responses. The Node demo server is the real implementation; run npm run examples to use it.

The npm run demo:live alias starts the same server for the live SSE page. A plain static server will not run the /demo/* endpoints.

Build & Test

The build uses Vite+ (Rolldown) in library mode:

npm run build   # dist/ + gzip budget gate
npm test        # build + type check + node:test + jsdom

Requires Node 22+ for the build; the shipped files run in modern browsers. Outputs: minu.min.js / minu.js (UMD), minu.esm.mjs (ESM), plus the minu.secure.* variants. Runtime dependencies: none.

Migrating from 1.1

  • init() no longer double-binds.
  • Events bubble.
  • Forms no longer need a manual preventDefault().
  • minu-loader / minu-content keep working.
  • reloadComponent() still works (deprecated).

Contributing

Contributions are welcome: bug reports, docs, examples, tests, and focused features. Read CONTRIBUTING.md for the setup, test, and PR checklist.

By contributing, you agree that your work is licensed under the MIT license.

Security reports go to [email protected], not the public issue tracker. See SECURITY.md.

License

MIT — see LICENSE. Contributions are accepted under the same license.