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>`;
},
});| Feature | Craft | Lit | Stencil |
|---|---|---|---|
| Bundle size | 10.5 KB | ~12 kB | ~60 kB+ toolchain |
| Signal-first runtime | |||
| Functional component setup | Partial | ||
| Typed prop helpers | Partial | ||
| Host binding helpers | Partial | Partial | |
| Form-associated helpers | Manual | Partial | |
| 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/craftsh
npm install @vielzeug/craftsh
yarn add @vielzeug/craftQuick 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 rawPropDefobjects - Setup returns an
HTMLResultdirectly: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(), andsyncAria()— 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
| Import | Purpose |
|---|---|
@vielzeug/craft | Core component API, directives, utilities, ripple re-exports |
@vielzeug/craft/devtools | debugFlush — verbose flush for timing diagnostics (dev only) |
@vielzeug/craft/observers | resizeObserver, intersectionObserver, mediaObserver, mutationObserver |
@vielzeug/craft/testing | mount, fire, user, waitFor, cleanup, and helpers |