Skip to content
craft logoCraftUI Primitives
Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, observers, and testing utilities.
v0.0.110.5 KB gzip Browser · Node.js · SSR · Deno
definehtmlcssproprefeachwhenclassMap +19 more →

Why Craft?

Craft keeps custom elements functional and signal-driven while giving you direct control over templates, lifecycle hooks, host bindings, and form-associated behavior.

ts
// Before — vanilla custom element boilerplate
class MyCounter extends HTMLElement {
  #count = 0;
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.#render();
  }
  #render() {
    this.shadowRoot!.innerHTML = `<button>${this.#count}</button>`;
    this.shadowRoot!.querySelector('button')!.onclick = () => {
      this.#count++;
      this.#render();
    };
  }
}
customElements.define('my-counter', MyCounter);

// After — Craft
import { define, html, signal } from '@vielzeug/craft';

define('my-counter', {
  setup() {
    const count = signal(0);
    return html`<button @click=${() => count.value++}>${count}</button>`;
  },
});
FeatureCraftLitStencil
Bundle size10.5 KB~12 kB~60 kB+ toolchain
Signal-first runtime (separate signals package)
Functional component setupPartial
Typed prop helpersPartial
Host binding helpersPartialPartial
Form-associated helpersManualPartial
Zero dependencies

Use Craft when you want typed, signal-driven custom elements with minimal runtime overhead and no framework lock-in.

Consider Lit when you need a mature ecosystem with wide community adoption and don't need signal-based reactivity.

Installation

sh
pnpm add @vielzeug/craft
sh
npm install @vielzeug/craft
sh
yarn add @vielzeug/craft

Quick Start

ts
import { computed, css, define, html, prop, signal } from '@vielzeug/craft';

define('my-counter', {
  props: {
    label: prop.string('Count'),
    step: prop.number(1),
  },
  styles: [
    css`
      :host {
        display: inline-grid;
        gap: 0.5rem;
      }
    `,
  ],
  setup(props, { bind, onMounted }) {
    const count = signal(0);
    const doubled = computed(() => count.value * 2);

    bind({ class: { 'is-positive': () => count.value > 0 } });

    onMounted(() => console.log('mounted'));

    return html`
      <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>
      <p>Doubled: ${doubled}</p>
    `;
  },
});

Features

  • Signal-first runtime with signal, computed, watch, batch, and related ripple APIs
  • Functional component authoring via define(tag, { props, setup, styles, formAssociated })
  • Props via prop.* helpers (prop.string, prop.number, prop.bool, prop.oneOf, prop.json, prop.data) or raw PropDef objects
  • Setup returns an HTMLResult directly: return html\...``
  • Lifecycle hooks — onMounted, onCleanup, onEvent, onElement, effect — accessed through the setup context bag (ctx)
  • Directives: each (keyed + snapshot modes), classMap, styleMap, when, model, raw
  • Host bindings via bind({ attr, class, style, on }) from setup context bag
  • Form-associated helpers: useField(), createFormContext(), and syncAria() — first-class public APIs for form-aware components
  • Observers (@vielzeug/craft/observers)
  • Testing utilities (@vielzeug/craft/testing) — mount, renderHook, fire, user, waitFor, cleanup
  • Debug utilities (@vielzeug/craft/devtools) — debugFlush() for diagnosing update timing

Package Entry Points

ImportPurpose
@vielzeug/craftCore component API, directives, utilities, ripple re-exports
@vielzeug/craft/devtoolsdebugFlush — verbose flush for timing diagnostics (dev only)
@vielzeug/craft/observersresizeObserver, intersectionObserver, mediaObserver, mutationObserver
@vielzeug/craft/testingmount, fire, user, waitFor, cleanup, and helpers

Documentation

See Also

  • Sigil for prebuilt accessible components powered by Craft.
  • Ripple for reactive state used inside Craft components.
  • Forge for typed form state that integrates with Craft.