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

@kobzi/gmconfig

v1.0.0

Published

Lightweight, multi-instance configuration dialog for Tampermonkey userscripts

Readme

gmConfig

A modern settings dialog for userscripts — define an object, get a themed panel.

TypeScript · native <dialog> · zero dependencies · light/dark · accessible · MIT

Quick start · Field types · Recipes · API · Full vs Lite · Migrating

Choose your size:   Full ~16 kB min / ~6.5 kB gzip  ·  Lite ~7.3 kB min / ~3.4 kB gzip


You write a plain object describing your settings. gmConfig renders a native <dialog>, persists the values, and hands them back — no iframe, no framework, no build step on your side (just drop in the IIFE). It leans on the browser instead of fighting it: native inputs, automatic light/dark, real focus-trapping, HTML5 validation.

const cfg = new gmConfig();

await cfg.init({
  username: { type: "text",     label: "Username",   default: "", required: true },
  interval: { type: "number",   label: "Refresh (s)", default: 30, min: 5, max: 300 },
  darkMode: { type: "checkbox", label: "Dark mode",  default: false },
  region:   { type: "select",   label: "Region", options: ["US", "EU", "Asia"], default: "EU" },
}, { title: "My Settings" });

const saved = await cfg.open();   // → true if saved, false if cancelled
console.log(cfg.get());           // → { username, interval, darkMode, region }

Need the smallest possible bundle? A Lite build ships the same gmConfig class with a trimmed feature set — swap the @require URL, change nothing in your code.


Highlights

  • Native everything<dialog>, color-scheme dark mode, accent-color, native pickers, HTML5 validation. Tiny CSS, no re-styling the browser.
  • Rich field set — every HTML5 input plus select, radio, multicheck, file, color, range, button, hidden.
  • Custom field types — build entirely new controls with render / get / set / destroy, the kind of extensibility you'd expect from a framework, not a userscript helper.
  • Structure — tabs, sections (collapsible), side-by-side rows, conditional fields (depends / showWhen).
  • Live & safeonChange, computed descriptions, async validation, save/close guards, unsaved-changes warning, and edits that stay a preview until you hit Save.
  • CSP & Trusted Types safe — no innerHTML anywhere and a constructed stylesheet, so it renders on locked-down sites where iframe/HTML-string libraries break.
  • Two builds, one source — full, or a smaller lite subset compiled from the same code. Same class name and API shape, swap the URL.

Why another config library?

GM_config was designed around 2009 browser capabilities — so it ships its own iframe, focus management, widgets, and styling infrastructure. Modern browsers now provide most of that natively:

  • <dialog> — modal stacking, backdrop, focus-trapping, Esc-to-close
  • color-scheme / accent-color — themed surfaces and tinted controls, no JS
  • native form controls + HTML5 validation — pickers, dropdowns, constraints for free

gmConfig keeps the familiar field-definition workflow (credit to GM_config by Mike Medley and contributors — a longtime staple of the ecosystem) but is not a fork: the implementation is rebuilt from scratch around these primitives. The result is a fraction of the code and CSS, with capabilities (custom controls, computed descriptions, async validation) that the original never had.


Install

Add one @require line. Pick the full build (everything) or the smaller lite build:

// ==UserScript==
// @name      My Script
// @require   https://cdn.jsdelivr.net/npm/@kobzi/gmconfig@latest/dist/gmConfig.iife.min.js
// @grant     GM.getValue          // persistence (full build falls back to localStorage if omitted)
// @grant     GM.setValue
// @grant     GM_registerMenuCommand   // optional — only for the `menu` option
// ==/UserScript==

Swap the @require for gmConfig.lite.iife.min.js to use the lite build. @latest is convenient for demos — pin a version (@kobzi/[email protected]) in production scripts.

Using a bundler? The npm package ships ESM + types:

import { gmConfig } from "@kobzi/gmconfig";        // full
import { gmConfig } from "@kobzi/gmconfig/lite";   // lite

CSP: gmConfig injects its stylesheet as a constructed stylesheet (adoptedStyleSheets), which style-src doesn't govern — so it stays styled even on CSP-strict sites, with no GM_addStyle grant needed. (Grant GM_addStyle only if you inject your own custom CSS with it.)

Persistence. The full build uses GM.getValue / GM.setValue, and falls back to localStorage (prefix gmcfg:) when GM storage is missing — e.g. @grant none or a bookmarklet. The fallback is per-origin, so settings aren't shared across domains. The lite build requires the GM storage functions (no fallback).


Quick start

The intro example at the top is the whole workflow: init(settings, options) loads persisted values, open() shows the dialog and resolves true on save / false on dismiss, get() / set() read and write values. One extra worth knowing from day one — add menu: true and gmConfig registers the Tampermonkey menu command for you (needs the GM_registerMenuCommand grant; pass a string for a custom label):

await cfg.init(settings, { title: "My Settings", menu: true });   // menu item "My Settings" → opens the dialog

Field types

Any HTML5 input type works directly — text, email, url, password, tel, search, number, range, date, time, datetime-local, month, week, color. On top of those ( = full-build only):

| Type | Value | Notes | |------|-------|-------| | text / email / url / … | string | required, pattern, minLength, maxLength, placeholder | | number / range | number | min, max, step | | checkbox | boolean | | | textarea | string | rows; ○ auto-grows with content (field-sizing, evergreen browsers) | | select | string | options (○ placeholder row) | | color | string | native swatch | | file | your choice | accept; handle the file via onFile | | button | — | click callback, not persisted | | hidden | any | persisted, no UI | | radio ○ | string | options — all choices shown at once | | multicheck ○ | string[] | dropdown of checkboxes (default: []) | | list ○ | object[] | repeatable records built from item — see Lists | | custom ○ | any | your own control via render — see Custom controls |

options takes an array (["a", "b"]), a label map ({ a: "Label A" }), or a sync/async provider (() => [...] / async () => [...]) — handy for choices fetched at open time. Text-like inputs also accept datalist (a list or sync/async provider) for native autocomplete suggestions.

Field options

Keys marked are full-build only.

| Key | Purpose | |-----|---------| | label, default | label text, initial value | | title | hover tooltip | | depends / showWhen | show conditionally — see Conditional fields | | description | help text under the control (○ (values) => string live form is full-only) | | disabled, readonly, save: false | non-editable, read-only, or excluded from storage | | attrs | extra attributes spread onto the control — autocomplete, inputmode, spellcheck, anything HTML supports | | section | group under a heading — a string (○ { title, desc?, collapsible?, collapsed? } form is full-only) | | tab ○ | group into a tab (untabbed fields render above the bar) | | row: "id" ○ | place consecutive same-id fields side by side (also works inside list items) | | labelPos ○ | "left" / "right" — label beside the control; "bottom" — caption-style under it | | datalist ○ | autocomplete suggestions for text inputs — a list or sync/async provider | | item, addLabel, itemTitle, reorder, max ○ | list field config — see Lists | | validate ○ | custom async/sync validation with inline errors |

The complete lite/full breakdown lives in Full vs Lite.


Recipes

Headings marked use full-build only features.

Sections

The plain-string form works in Lite; the { title, desc, collapsible } object form is full-only.

name:   { type: "text", label: "Name", default: "", section: "Profile" },
apiKey: { type: "password", label: "API key", default: "",
          section: { title: "API", desc: "Stored locally", collapsible: true, collapsed: true } },  // ○ object form

Tabs & rows ○

name:  { type: "text",   label: "Name",  default: "", tab: "Profile" },
theme: { type: "select", label: "Theme", options: ["system","light","dark"], default: "system", tab: "Appearance" },

start: { type: "time", label: "Start", default: "09:00", row: "shift" },
end:   { type: "time", label: "End",   default: "17:00", row: "shift" },

Conditional fields

useProxy: { type: "checkbox", label: "Use proxy", default: false },
proxyUrl: { type: "url", label: "Proxy URL", default: "", showWhen: { useProxy: true } },
// or a predicate:
proxyUrl: { type: "url", label: "Proxy URL", default: "", depends: v => v.useProxy === true },

Validation ○

Runs on Save, after native HTML5 constraints. Return an error string to block (shown inline), or nothing if valid:

username: { type: "text", label: "Username", default: "",
            validate: v => /^[a-z0-9_]{3,}$/.test(v) ? undefined : "3+ chars, a-z 0-9 _ only" },
port:     { type: "number", label: "Port", default: 8080,
            validate: async v => (await isPortFree(v)) ? undefined : "Port in use" },

Dynamic options

setField works in both builds; this example uses multicheck, which is full-only — swap in select for Lite.

await cfg.init({ tags: { type: "multicheck", label: "Tags", options: {}, default: [] } }, {
  onOpen: async () => {
    const tags = await fetch("/api/tags").then(r => r.json());
    cfg.setField("tags", { options: Object.fromEntries(tags.map(t => [t.id, t.name])) });
  },
});

Or skip setField entirely and hand options a provider — select / radio / multicheck fill themselves (and re-apply the stored value when an async provider resolves):

tags: { type: "multicheck", label: "Tags", default: [], options: () => fetch("/api/tags").then(r => r.json()) },

Lists ○

type: "list" renders a repeatable array of records. Each item is a mini-form built from item (a normal field map — including showWhen, row, datalist, even a nested list). The value is an object[]; gmConfig handles add / remove / reorder, persistence, and revert.

endpoints: {
  type: "list", label: "API endpoints", default: [], reorder: true, addLabel: "+ Endpoint",
  itemTitle: (v, i) => v.name || `#${i + 1}`,        // collapsed-row header
  item: {
    name:   { type: "text",   label: "Name", row: "r" },
    method: { type: "select", label: "Method", options: ["GET", "POST"], default: "GET", row: "r" },
    url:    { type: "url",    label: "URL" },
    auth:   { type: "checkbox", label: "Needs auth", default: false },
    token:  { type: "text",   label: "Token", showWhen: { auth: true } },  // per-item conditional
  },
},

item field options supported inside a list: every native input, select / radio / multicheck, datalist, showWhen / depends, row, custom controls (render / get / set / destroy — cleanup hooks fire on item removal and dialog close), and nested list. (tab and section are not applied inside items.) A max caps the item count — the add button disables at the limit.

File processing

A file field hands you the picked File and a setter — you decide what (if anything) to store:

importList: {
  type: "file", label: "Import list", accept: ".txt",
  onFile: (file, set) => file.text().then(t => set(t.split("\n").length)),  // store the line count
},

Save & close guards ○

await cfg.init(settings, {
  onBeforeSave:  async v => await serverAccepts(v.apiKey),     // return false to block the save
  onBeforeClose: () => confirm("Discard unsaved changes?"),    // return false to keep it open
});

Edits are a preview until Save. Field changes live in the DOM only and are written on Save (or closeOnSave). Dismissing (Cancel / X / Esc / backdrop) discards them and reverts to the last persisted state. A beforeunload warning fires if you try to leave the page with unsaved edits. (Full build; Lite simply doesn't write live edits until save, so there's nothing to revert.)

Custom controls ○

Bring your own control with render (build the node); get / set read and write its value (both default to el.value, so a wrapped input needs only render), and destroy cleans up timers/observers. gmConfig.type() packages a reusable control:

const colorType = gmConfig.type({
  render: f => Object.assign(document.createElement("input"), { type: "color", value: f.value ?? "#000000" }),
});

await cfg.init({
  bg:     { label: "Background", default: "#ffffff", ...colorType },
  accent: { label: "Accent",     default: "#0a53f8", ...colorType },

  clock: {                                  // a widget that owns a timer cleans up via destroy()
    label: "Clock", save: false,
    render: () => { const el = document.createElement("span"); el._id = setInterval(() => el.textContent = new Date().toLocaleTimeString(), 1000); return el; },
    get: () => null,
    destroy: el => clearInterval(el._id),
  },
});

Language switch (reinit) ○

await cfg.init(buildSettings("en"), {
  onChange: (key, value) => {
    if (key === "lang") cfg.reinit(buildSettings(value));   // hot-swap labels, keep values + active tab
  },
});

API

Rows marked are full-build only — in Lite they exist but are harmless no-ops (same API shape, nothing crashes). Everything else works identically in both.

Methods

| Method | Returns | Description | |--------|---------|-------------| | init(settings, opts?) | Promise<Vals> | Initialize and load persisted values | | open(tab?) | Promise<boolean> | Show the dialog (optionally on a named tab); resolves true if saved | | get(key?) | value / Vals | One value by key (typed: get<string>("k")), or all values | | set(key, value) | boolean | Write a value (updates DOM, fires onChange); false if key unknown / not settable | | save() | Promise | Persist current values | | reset() | Promise | Restore defaults and persist | | importExport() | Promise<boolean> | Backup/restore via a JSON prompt; true if applied | | setField(key, props) | — | Merge new props into a field and re-render it | | destroy() | — | Close and release everything (fires custom destroy hooks) | | reinit(settings, title?) ○ | — | Hot-swap definitions, keeping live values and the active tab | | getElement(key) | HTMLElement | A field's live DOM element, or null when closed | | setAccent(color) ○ | — | Set accent live (any CSS color, or null to reset); auto-contrasts the Save text | | setTheme(t) ○ | — | "light" / "dark" / "system" | | gmConfig.type(def) ○ | FieldType | (static) package a reusable custom control |

Properties

| Property | Description | |----------|-------------| | ready | Promise<Vals> resolved once storage loads (same as init's return; rejects if the read throws) | | isOpen | true while shown | | title | resolved panel title |

Options (init's 2nd argument)

| Option | Description | |--------|-------------| | title | Panel title (default: the userscript @name, else "Settings") | | storageKey | Storage key (default: the title) | | menu | true / "Label" — register a userscript menu command that opens the dialog (needs the GM_registerMenuCommand grant) | | onChange | (key, value) on every live edit and set() | | onOpen / onSave / onClose / onReset | Lifecycle hooks | | closeOnSave ○ | false keeps the panel open after saving (default true) | | accent ○ | Accent color (any CSS color) | | width ○ | Panel width (CSS length or px number); overrides the default clamp(320px,90%,460px) | | onBeforeSave ○ | (values) => boolean \| void — after validation; false blocks the save | | onBeforeClose ○ | (values) => boolean \| void — on dismiss when changed; false keeps it open | | footerContent ○ | Node, string, or (cfg) => … shown left of the action buttons |


Theming & accessibility

  • Dark mode follows the OS via color-scheme / light-dark().
  • Keyboard: <dialog> traps focus; Esc dismisses (respecting onBeforeClose); tabs navigate with ← → Home End and carry ARIA tablist/tab/tabpanel roles.
  • Validation uses the browser's own messages, plus inline custom errors from validate.

Accent tints native controls, the active tab, and the Save button:

await cfg.init(settings, { accent: "#e74c3c" });
cfg.setAccent("#22c55e");   // live
cfg.setAccent(null);        // reset

Quick theming via two CSS variables on .gc-p (--gc-a accent, --gc-r radius), or target gc-* classes directly:

GM_addStyle(`.gc-p { --gc-a: #e74c3c; --gc-r: 0 }`);   // red accent, square corners

Full vs Lite

Both builds are compiled from the same source and expose the same gmConfig class — choose one by @require URL, change nothing else:

// @require https://cdn.jsdelivr.net/npm/@kobzi/gmconfig@latest/dist/gmConfig.iife.min.js        // full — ~16 kB min / ~6.5 kB gzip
// @require https://cdn.jsdelivr.net/npm/@kobzi/gmconfig@latest/dist/gmConfig.lite.iife.min.js   // lite — ~7.3 kB min / ~3.4 kB gzip

Lite covers the common case: native fields, button, hidden, flat section, depends / showWhen, file, attrs, tooltips, import/export, reset, onChange, menu. Full adds everything marked throughout this README — radio / multicheck / list, tabs / rows / labelPos, custom controls, accent + theme + width, async validate, computed descriptions, collapsible sections, reinit, the onBefore* guards, the unsaved-changes warning, async/map options + datalist, and a localStorage fallback (lite is GM-storage-only).

Reach for lite when you need a handful of plain settings and the smallest bundle; full otherwise. Because both builds share one source, the API shape is identical: full-only methods exist in lite as harmless no-ops (calling reinit() won't crash), and TypeScript will let you write full-only field props — lite silently ignores them at runtime. Anything lite supports behaves exactly as documented above.


Demo

demo/demo.user.js is a ready-to-run, heavily commented showcase of the full build: every field type including a list of highlight rules (reorder, max, per-item showWhen), tabs / sections / rows, depends chains, async validate, computed descriptions, async options / datalist providers, attrs, labelPos, the menu shortcut, five custom controls (star rating, tag input, live clock, hotkey recorder, CSV loader with upload + fetch-from-URL), live theming, and reinit language switching. Paste it into Tampermonkey — it opens on first run.


Migrating from GM_config

// Before — sizzlemctwizzle/GM_config
new GM_config({
  id: "MyConfig", title: "Settings",
  fields: {
    name:  { label: "Name",  type: "text",     default: "User" },
    count: { label: "Count", type: "int",      default: 5 },
    on:    { label: "On",    type: "checkbox", default: true },
  },
  events: { save() { console.log(this.get("name")); } },
}).open();

// After — gmConfig
const cfg = new gmConfig();
await cfg.init({
  name:  { type: "text",     label: "Name",  default: "User" },
  count: { type: "number",   label: "Count", default: 5, min: 0 },   // 'int' → 'number' + min
  on:    { type: "checkbox", label: "On",    default: true },
}, {
  title: "Settings",
  storageKey: "MyConfig",                              // match the old id for storage compatibility
  onSave: () => console.log(cfg.get("name")),
});
cfg.open();

| GM_config | gmConfig | |---|---| | new GM_config({ id, fields, events }) | new gmConfig() + await cfg.init(fields, opts) | | type: "int" / "float" | type: "number" + min / step | | type: "unsigned int" | type: "number", min: 0 | | events: { save, open, close } | onSave / onOpen / onClose options | | id for storage | storageKey option | | css: in constructor | GM_addStyle(...) on gc-* classes | | sync API · iframe | async (await) · native <dialog> |


Build

npm install && npm run build

Both variants — full (dist/gmConfig.iife.min.js) and lite (dist/gmConfig.lite.iife.min.js) — compile from the single source src/gmConfig.ts: an esbuild --define:FULL=true/false flag selects the build, and dead-code elimination strips everything the lite build doesn't use. Each variant ships as IIFE (minified + readable dev) and ESM, with shared TypeScript declarations. npm run build:size prints raw + gzip sizes for both bundles.


License

MIT