@johnhenry/domkit
v0.1.1
Published
Small, independent custom elements and DOM utilities -- drop-in UI widgets, media-query-driven containers, persisted UI state, declarative loaders -- shipped as source, no build step.
Downloads
1,112
Maintainers
Readme
domkit
Small custom elements and DOM utilities that work straight from a CDN, with no build step and no framework. Every module is independent, so you load only what you use.
<script type="module" src="https://esm.sh/@johnhenry/domkit/tabbed-ui/global.mjs"></script>
<tabbed-ui>
<div><button>One</button><button>Two</button></div>
<section>First panel</section>
<section>Second panel</section>
</tabbed-ui>Every element is held to the same contract: it behaves like a native
HTML element (attributes, properties, events, forms, hidden, keyboard,
and accessibility all work the way they do for built-ins), and it's tested
in Chromium, Firefox, and WebKit. See docs/principles.md.
Every stable element meets it.
Full documentation: opensource.johnhenry.me/domkit
· Live gallery: demo/ (see Development)
Install
npm install @johnhenry/domkitimport "@johnhenry/domkit/tabbed-ui/global.mjs"; // registers <tabbed-ui>
import TabbedUI from "@johnhenry/domkit/tabbed-ui"; // or just the class
import liveQuerySelector from "@johnhenry/domkit/live-query-selector";Or skip installing: every path above also works as
https://esm.sh/@johnhenry/domkit/<path>.
Find what you need
Interface pieces
| Module | What it's for | global.mjs registers |
|---|---|---|
| tabbed-ui | Tabs and panels from plain children, matched by position | <tabbed-ui> |
| stylable-select | A listbox whose options you can fully style | <stylable-select> |
| infinite-combo-box | Autocomplete with paged ("infinite") results: filters its own options, or searches a URL or function as you type | <infinite-combo-box> |
| swipe-input | Swipe gestures that send invoker commands, one per direction | <swipe-input> |
| gamepad-input | Game controller buttons that send invoker commands | <gamepad-input> |
| hot-key | Keyboard shortcuts (mod+k, /) that toggle a native <dialog> or popover, or run any invoker command | <hot-key> |
| drill-menu | A list that drills into sub-screens and back (settings menus, mobile nav), optionally synced to the URL hash | <drill-menu> |
| code-color | Syntax highlighting (JS, CSS, HTML) that never changes your markup, themable with CSS | <code-color> |
| code-editor | An editable, highlighted code field that works like a <textarea> (forms, events, selection), with indent, auto-indent, and bracket pairs on the native undo stack | <code-editor> |
Responding to screen size: matchable/
| Module | What it's for | global.mjs registers |
|---|---|---|
| query-container | Change the element that wraps content (ul → ol, …) by media query | <query-container> |
| attribute-provider | Change children's classes, styles, and attributes by media query | <attribute-provider> |
Both also have a container mode (container attribute): the same
queries, evaluated against an element's size instead of the viewport.
Remembering a user's choice (e.g. a theme toggle): cyclable/
| Module | What it's for | global.mjs registers |
|---|---|---|
| attribute-cycler | Buttons that cycle a persisted class or attribute (theme, density), synced across tabs | <attribute-cycler> |
| localstorage-attribute-cycler | The same, as a JS function | |
| localstorage-cycler | The engine: a persisted value with next/previous/peek/set | |
Wiring things up from markup instead of scripts: definable/
| Module | What it's for | global.mjs registers |
|---|---|---|
| define-component | Register a custom element from a module URL or from inline markup | <define-component> |
| polyfill-window | Load a module onto window | <polyfill-window> |
| until-window-load | Hide content until the page and its <define-component>s have loaded | (strips the until-window-load class) |
| definetag | Curried customElements.define | |
Timing, collections, and small helpers
| Module | What it's for | global.mjs registers |
|---|---|---|
| delay | await delay(ms), or await delay({ fps }) to wait one frame period on an animation frame | |
| frame-timer | A clock element: steady tick events with play()/pause(), like a media element | <frame-timer> |
| live-query-selector | querySelectorAll that stays current, with a change event | |
| clamp | clamp(min, max)(value) | |
Bootstrapping an app: hydratable/
| Module | What it's for |
|---|---|
| hydratable | A run-once async hydrate() (with dehydrate() to undo it) for any object |
| hydratable/mounts | Find or create the element at the start/end of <body> to render an app into |
Graphics
Plots and <chernoff-face> moved to @johnhenry/data-plot.
| Module | What it's for | global.mjs registers |
|---|---|---|
| draw-svg | SVG strokes that draw themselves in, CSP-safe and reduced-motion aware | <draw-svg> |
Pixel effects: moved to @johnhenry/canvas-fx
<pixel-canvas>, <pixel-sprite>, and the pixel effects (mosaic,
palettes, CRT, shaders, …) are their own package now:
@johnhenry/canvas-fx.
Recipes
Complete pages in examples/ that combine modules the way a
real site would. Each is plain HTML you can read top to bottom:
- Theme switcher:
attribute-cycler+color-scheme, synced across tabs. - Command palette:
hot-key+infinite-combo-boxon ⌘K. - Settings panel: one
<form>acrossdrill-menuscreens, withstylable-select,infinite-combo-box, andattribute-cyclersubmitting like native controls. - Documentation page:
tabbed-uicode samples highlighted bycode-color, plusquery-container. - Game loop:
frame-timerdriving an animation.
How the package is laid out
- One directory per module, under
src/. Related modules are grouped one level deeper (matchable/,cyclable/,definable/,hydratable/), and the group is part of the import path. @johnhenry/domkit/<module>imports the module'sindex.mjs: an element class (unregistered) or a function. Any file inside can be imported by its full path, e.g.@johnhenry/domkit/tabbed-ui/global.mjs.global.mjsregisters the element under the tag in the tables above. Import the class instead if you want a different tag name.- No root import.
import "@johnhenry/domkit"doesn't exist, on purpose. These modules share no state or API, and a barrel would make every page download all of them, especially from a CDN where nothing tree-shakes. - Source is what ships. There's no build step: modern JS, ES modules only.
No dependencies
domkit has no runtime dependencies, so you can serve src/ directly, copy
a module's directory into your project, or load it from any CDN, with no
import map and no build step.
Content-Security-Policy
No module evaluates strings as code, so domkit works under a strict Content-Security-Policy.
Theming
Every element's optional index.css reads one shared set of design
tokens (accent, highlight, border, radius, focus ring, popup surface,
disabled opacity). Load theme.css for ready-made, light-dark()-aware
values, or set the tokens yourself to theme every element at once:
<link rel="stylesheet" href="https://esm.sh/@johnhenry/domkit/theme.css" />
<style>
:root { --domkit-accent: rebeccapurple; --domkit-radius: 0; }
</style>Editor support
The package ships custom-elements.json, a
standard manifest of every element's tag, attributes, properties, events,
and CSS custom properties, and which module registers each tag (its
global.mjs, as a custom-element-definition export). Tools that read it
(Storybook, many IDE plugins, page builders) work out of the box. For VS Code's HTML autocomplete and hover
docs, add the generated data file to your settings:
{ "html.customData": ["./node_modules/@johnhenry/domkit/vscode.html-custom-data.json"] }TypeScript users get declarations for each element class and its
HTMLElementTagNameMap entry, so document.querySelector("tabbed-ui") is
typed, and the plain-function modules have declarations too.
The generated element reference lists every attribute, property, method, event, and CSS custom property.
Stability
Every module is stable: documented, held to
the principles, tested in Chromium, Firefox, and
WebKit, and versioned with semver. While the version is 0.x, a breaking
change can only land in a minor release (0.1 → 0.2), never a patch,
and it's always listed in CHANGELOG.md under
Changed (breaking).
Development
npm testThis runs, in order: a parse check of every module
(scripts/check-syntax.mjs), a reference check that every relative
import, Markdown link, and documented @johnhenry/domkit/… path resolves
(scripts/check-links.mjs), ESLint's no-undef rule (the one rule
configured, see eslint.config.mjs), a TypeScript check of the generated
declarations, and the fast behavioral tests in test/ under
happy-dom.
npx playwright install # once
npm run test:browser # test/browser/, in Chromium, Firefox, and WebKit
npm run manifest # regenerate custom-elements.json, types, editor dataThe live gallery runs most modules at once, each in its own iframe:
npm run servethen open http://localhost:4719/demo/. Module imports don't resolve over
file://. Each module directory also has its own demo.html/demo.htm.
See AGENTS.md for the full verification loop and the
repo's known gotchas.
Related
@johnhenry/domableconverts between HTML text, DOM nodes, and React-element-shaped objects, and builds custom-element classes from HTML strings. domkit used to depend on it, and is now dependency-free.
History
domkit was extracted from johnhenry/lib,
a personal collection of hot-linked modules, in September 2026, and
consolidated along the way: duplicates of domable were dropped, related
modules were grouped into families, and a series of bugs were fixed (most
of them found by actually running the modules). CHANGELOG.md
has the whole story.
License
MIT
