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 🙏

© 2025 – Pkg Stats / Ryan Hefner

handcraft

v0.14.0

Published

A tiny front-end framework using a fluent interface for constructing UI. It also has shallow reactivity.

Downloads

429

Readme

Handcraft

A tiny front-end framework using a fluent interface for constructing UI. It also has shallow reactivity. Install from NPM or get it from a CDN like jsDelivr and add it to your import map.


API

reactivity.js

Where everything for creating reactive state resides.

watch(object)

Pass it an object (arrays supported) and it returns the object wrapped in a proxy that will track reads of properties in effects, and writes/deletions will rerun those effects.

effect(callback)

Pass it a callback to do operations that should get rerun when watched objects are changed. It's for things not covered by the DOM API. Eg. setting localStorage or calling methods on DOM elements. Only properties that are later changed will trigger a rerun. Internally this same method is used anywhere a callback known as an effect is allowed.

import {watch, effect} from "handcraft/reactivity.js";

let state = watch({foo: 0});

effect(() => {
	console.log(state.foo);
});

setInterval(() => {
	state.foo += 1;
}, 10_000);

dom.js

Where everything for creating DOM elements resides.

html, svg, math

These are proxies of objects with properties that are functions referred to as tags that when run return an instance of HandcraftElement. There are three because HTML, SVG, and MathML all require different namespaces when creating a DOM element.

HandcraftNode, HandcraftElement, and HandcraftRoot

Usually you won't use these directly unless you want to write your own methods. They are exported so that methods can be added to their prototype. HandcraftElement and HandcraftRoot are sub-classes of HandcraftNode so they inherit all methods on HandcraftNode.

import {HandcraftElement} from "handcraft/dom.js";

HandcraftElement.prototype.text = function (txt) {
	this.element.textContent = txt;

	return this;
};

Below node refers to methods on HandcraftNode, element refers to methods on HandcraftElement, and shadow those on HandcraftRoot.

node.deref()

A method on HandcraftNode instances that returns the underlying DOM element.

$(node)

Wraps a DOM node in the fluent interface.

import {$} from "handcraft/dom.js";

assert($(document.body).deref() === document.body);

define.js

Contains the API for creating custom elements.

define(name)

Pass it the name of your custom element. It returns a definition that is also a tag.

definition.connected(callback)

The callback is run in the custom element's connectedCallback.

import {$} from "handcraft/dom.js";
import {define} from "handcraft/define.js";

$(target).append(
	define("hello-world").connected((host) => {
		host.text("hello world!");
	})
);

definition.disconnected(callback)

The callback is run in the custom element's disconnectedCallback.

dom/*.js

Every module in the "dom" directory adds a method to the HandcraftNode, HandcraftElement, or HandcraftRoot prototype. Import the file to add the method. For instance to use styles(styles) import dom/styles.js.

node.append(...children)

Append children to a node. Each child can be a string, a DOM element, or a node. Children are initially appended, but on update their position is maintained. Returns the node for chaining. Though it's not necessary you may want to also import dom/_nodes.js, to reduce network waterfall.

element.aria(attrs)

Set aria attributes. Accepts an object. Values can be effects. Returns the element for chaining.

element.attr(key, value)

Set an attribute. The second parameter can be an effect. Returns the element for chaining. This method also can be used to read an attribute if no value is provided.

element.classes(...classes)

Set classes. Accepts a variable number of strings and objects. With objects the keys become the class strings if their values are truthy. Values can be effects. Returns the element for chaining.

root.css(css)

Adds a stylesheet to the adoptedStyleSheets of a HandcraftRoot instance. The css can be an effect. Returns the root for chaining.

element.data(data)

Set data attributes. Accepts an object. Values can be effects. Returns the element for chaining.

import "handcraft/dom/aria.js";
import "handcraft/dom/attr.js";
import "handcraft/dom/classes.js";
import "handcraft/dom/data.js";
import {html} from "handcraft/dom.js";

let {div} = html;

div()
	.aria({
		label: "example",
	})
	.attr("role", "foo"})
	.classes({
		"foo": () => state.foo
	})
	.data({"foo": "example"});

node.effect(callback)

Run an effect. The callback is passed the DOM element. Returns the node for chaining.

import "handcraft/dom/effect.js";
import {html} from "handcraft/dom.js";
import {watch} from "handcraft/reactivity.js";

let {dialog} = html;
let state = watch({
	modalOpen: false,
});

dialog().effect((el) => {
	if (state.modalOpen) {
		el.showModal();
	} else {
		el.close();
	}
});
node.find(selector)

Find children based on a selector. Returns an iterable with each item wrapped in the DOM API.

import "handcraft/dom/find.js";
import {$} from "handcraft/dom.js";

let divs = $(document.body).find("div");

for (let div of divs) {
	div.classes("bar");
}

node.observe()

Returns an observer that uses a MutationObserver backed way to read attributes and find descendants.

observer.attr(key)

Read an attribute.

import "handcraft/dom/append.js";
import "handcraft/dom/observe.js";
import "handcraft/dom/text.js";
import {html} from "handcraft/dom.js";
import {define} from "handcraft/define.js";

let {div} = html;

define("hello-world").connected((host) => {
	let observed = host.observe();

	host.append(div().text(() => `hello ${observed.attr("name")}!`));
});
observer.find(selector)

Find children.

import "handcraft/dom/observe.js";
import {$} from "handcraft/dom.js";
import {effect} from "handcraft/reactivity.js";

let observed = $(document.body).observe();
let divs = observed.find("div");

effect(() => {
	for (let div of divs) {
		div.classes("bar");
	}
});

node.prepend(...children)

Like append, but for prepending children to a node. Each child can be a string, a DOM element, or a node. Children are initially prepended, but on update their position is maintained. Returns the node for chaining. Though it's not necessary you may want to also import dom/_nodes.js, to reduce network waterfall.

node.on(name, callback, options = {})

Set an event handler. Has the same signature as addEventListener but the first parameter can also be an array to set the same handler for multiple event types. Returns the node for chaining.

node.once(name, callback, options = {})

Set an event handler. Has the same signature as node.on but it automatically adds once: true to the options. Returns the node for chaining.

node.prop(key, value)

Set a property. The second parameter can be an effect. Returns the node for chaining.

element.shadow(options = {mode: "open"})

Attaches and returns a shadow, or returns an existing one. The returned shadow instance is wrapped in the HandcraftRoot API.

element.styles(styles)

Set styles. Accepts an object. Values can be effects. Returns the element for chaining.

node.text(text)

When you need to set one text node, use text instead of append or prepend. The parameter can be a string or an effect. Returns the node for chaining. This method also can be used to read text if no argument is provided.

import "handcraft/dom/append.js";
import "handcraft/dom/on.js";
import "handcraft/dom/prop.js";
import "handcraft/dom/shadow.js";
import "handcraft/dom/styles.js";
import "handcraft/dom/text.js";
import {html} from "handcraft/dom.js";
import {define} from "handcraft/define.js";

let {button, span} = html;

define("hello-world").connected((host) => {
	let shadow = host.shadow();

	shadow.append(
		button()
			.prop("type", "button")
			.styles({
				color: "white",
				background: "rebeccapurple",
			})
			.on("click", () => console.log("clicked!"))
			.append(span().text("click me"))
	);
});

each.js

Each is a way to create reactive lists.

each(list)

Entry point for this API. Pass it a watched array. Returns a collection that is iterable, having a Symbol.iterator method.

collection.filter(callback)

The callback will be run for each item in the collection. Return a truthy value to move onto the map step. It is passed value and index. Both are functions, but value proxies to the underlying item.

collection.map(callback)

The callback will be run for each item in the collection that passes the filter step. It should return an element. It is passed value and index. Both are functions, but value proxies to the underlying item. Do not use destructuring assignment with the value between effects, because they will not be rerun if the item is swapped out since the callback when run in append or prepend is only run once per index. This avoids destroying DOM elements only to rebuild them with new data.

import "handcraft/dom/on.js";
import "handcraft/dom/append.js";
import "handcraft/dom/text.js";
import {html} from "handcraft/dom.js";
import {each} from "handcraft/each.js";
import {watch} from "handcraft/reactivity.js";

let {button, ul, li} = html;
let list = watch([]);

button().on("click", () => {
	list.push(Math.floor(Math.random() * 20) + 1);
});

ul().append(
	each(list)
		.filter((value) => value() % 2)
		.map((value) => li().text(value()))
);

when.js

When is used to conditionally render an element.

when(callback)

Entry point for this API. Pass it a function that should return a boolean that controls whether the element should be rendered. The function is passed the previous result of the callback being called. Returns a conditional.

conditional.show(callback)

The callback should return the element to be rendered.

conditional.fallback(callback)

The callback should return a different element to be rendered if the the when callback returns false.

import "handcraft/dom/append.js";
import "handcraft/dom/on.js";
import "handcraft/dom/text.js";
import {html} from "handcraft/dom.js";
import {watch} from "handcraft/reactivity.js";
import {when} from "handcraft/when.js";

let {span, button} = html;
let state = watch({
	clicked,
});

button()
	.on("click", () => {
		state.clicked = true;
	})
	.append(
		when(() => state.clicked)
			.show((entry) => span().text("clicked!"))
			.fallback("not clicked")
	);

prelude/min.js

For convenience, a module that exports all of dom and reactivity and imports attr, append, on, prop, and text. The minimum you'd need to get started.

prelude/all.js

Exports all other exports, and imports all dom/*.js files. Probably only use this for demos.


Inspiration

A lot of the API is inspired by Rust and its ecosystem. The rest is the latest iteration of ideas I've had since around 2015. I need to mention the following as inspiration though.