Package Entry Points
@vielzeug/craftit— core API (stable)@vielzeug/craftit/controls— headless interaction APIs@vielzeug/craftit/observers— browser observer APIs@vielzeug/craftit/directives— template directives@vielzeug/craftit/testing— testing helpers
API At a Glance
define()— registers a component definition under a custom-element tag.html— creates reactive templates; avoid building templates with string concatenation.onMount()— runs setup logic tied to component lifecycle; return cleanup when you attach listeners.
Core Component API
define(tag, definition)
define<Props, Events>(tag: string, definition: ComponentDefinition<Props, Events>): string;Registers a custom element and returns the tag name.
If the tag already exists, Craftit keeps the existing registration and returns the tag.
Field Authoring Controls
From @vielzeug/craftit/controls:
createTextFieldControl(options)— text field state, ids, assistive state, attrs, and lifecycle wiring.mountTextFieldLifecycle(options)— input/change/blur wiring + validation trigger bridge.
import { define, html, signal } from '@vielzeug/craftit';
define('my-counter', {
setup() {
const count = signal(0);
return html`<button @click=${() => count.value++}>${count}</button>`;
},
});Access host/shadow via currentRuntime() inside setup() when needed.
const { el: host, shadowRoot: shadow } = currentRuntime();For host attributes, classes, and host listeners, use host.bind(...) from the setup context.
Runtime Helpers
Lifecycle
onMount(fn)— run logic after mount.onCleanup(fn)— register cleanup.onError(fn)— component-scoped error handler.createCleanupSignal()— manage replaceable cleanup functions and dispose on unmount.onElement(ref, fn, options?)— element-scoped effect for nullable refs.onCleanup()is component-aware: inside component setup/mount it runs on unmount; outside component context it delegates to stateit's effect cleanup behavior.
Reactivity Wrappers
effect(fn, options?)— component-aware wrapper around stateiteffect.watch(source, cb, options?)— component-aware watcher.
Signatures:
watch<T>(source: ReadonlySignal<T>, cb: (value: T, prev: T) => void, options?: WatchOptions<T>): Subscriptionwatch(sources: ReadonlyArray<ReadonlySignal<unknown>>, cb: () => void, options?: WatchOptions<unknown>): Subscription
DOM/Event Utilities
handle(target, event, listener, options?)— listener with auto-cleanup.fire.custom(target, type, options?)— dispatches aCustomEvent.fire.mouse(target, type, options?)— dispatches aMouseEvent.fire.keyboard(target, type, options?)— dispatches aKeyboardEvent.fire.focus(target, type, options?)— dispatches aFocusEvent.fire.touch(target, type, options?)— dispatches aTouchEventwhen available, otherwiseCustomEvent.fire.event(target, event)— dispatches a prebuilt event instance.aria(attrs)oraria(target, attrs)— reactive ARIA attributes (false,null, andundefinedremove the attribute).host.bind({ attr: ... }),host.bind({ class: ... }),host.bind({ on: ... })— host attribute/class/listener wiring from setup context.
Prefer template @event bindings for inner DOM nodes; use host.bind({ on: ... }) or handle() for host-level interactions.
Template and Styling
html
html(strings: TemplateStringsArray, ...values: unknown[]): HTMLResult;Tagged template with reactive bindings.
Template event bindings support modifiers:
@click.stop=${handler}@submit.prevent=${handler}@click.self=${handler}@keydown.once=${handler}- listener options:
.capture,.passive,.once
css
css(strings: TemplateStringsArray, ...values: unknown[]): CSSResult;Returns { content: string; toString(): string }.
Props API
define<Props>(tag, { props })
Declare component props directly in define using plain defaults. The Props generic defines the setup signal types.
type ButtonProps = {
count?: number;
disabled?: boolean;
label?: string;
variant?: 'primary' | 'secondary';
};
define<ButtonProps>('my-button', {
props: {
count: 0,
disabled: false,
label: 'Button',
variant: 'primary',
},
setup({ props }) {
return html`
<button ?disabled=${props.disabled}>
${props.label}
<span class=${`variant-${props.variant}`}>${props.count}</span>
</button>
`;
},
});Optional props use undefined defaults:
type ButtonProps = {
description?: string;
size?: 'sm' | 'md' | 'lg';
};
define<ButtonProps>('my-button', {
props: {
description: undefined,
size: undefined,
},
setup({ props }) {
return html`${props.description}`;
},
});When you need custom parsing or reflection control, use a PropDef object inline:
define<{ count?: number; error?: string }>('my-counter', {
props: {
count: { default: undefined, type: Number },
error: { default: '', omit: true },
},
setup({ props }) {
return html`${props.count}`;
},
});prop(name, defaultValue, options?)
Low-level API for reactive property bindings. Typically use inline component<Props>({ props }) defaults instead.
prop<T>(name: string, defaultValue: T, options?: PropOptions<T>): Signal<T>;PropOptions<T>:
parse?: (value: string | null) => T— custom attribute parsingreflect?: boolean— reflect property to attribute (default:true)omit?: boolean— remove attribute instead of setting to""for empty stringstype?: StringConstructor | NumberConstructor | BooleanConstructor | ArrayConstructor | ObjectConstructor
Event typing with define<Props, Events>()
Declare component event payloads with the second define generic. This keeps setup({ emit }) fully typed without a separate emits schema.
type Events = {
select: { value: string };
close: void;
};
define<{ value?: string }, Events>('my-example', {
props: {
value: undefined,
},
setup({ emit, props }) {
emit('select', { value: props.value.value ?? 'alpha' });
emit('close');
return html``;
},
});Slots and Emits
setup-context emit
setup({ emit }) receives a typed emit function.
type EmitFn<T extends Record<string, unknown>> = {
<K extends KeysWithoutDetail<T>>(event: K): void;
<K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): void;
};
type KeysWithoutDetail<T extends Record<string, unknown>> = {
[P in keyof T]: [T[P]] extends [void | undefined | never] ? P : never;
}[keyof T];Example:
type Events = { open: void; select: { value: string } };
emit('open');
emit('select', { value: 'alpha' });setup-context slots
setup({ slots }) receives first-class slot signals.
setup({ slots }) {
slots.has().value;
slots.has('header').value;
slots.elements('trigger').value;
slots.elements().value;
}slots.has(name?)— whether the given slot currently has assigned elementsslots.elements(name?)— flattened assigned elements for a slot name
Omit the name to address the default slot.
Context API
createContext<T>(description?)provide(key, value)inject(key)/inject(key, fallback)syncContextProps(ctx, props, keys)
Types:
InjectionKey<T>
Form-Associated API
defineField(options, callbacks?)
defineField<T>(options: FormFieldOptions<T>, callbacks?: FormFieldCallbacks): FormFieldHandle;Requires define('tag-name', { formAssociated: true, ... })); otherwise Craftit throws an explicit runtime error.
Types:
FormFieldOptions<T>value: Signal<T> | ReadonlySignal<T>toFormValue?: (value: T) => string | File | FormData | nulldisabled?: Signal<boolean> | ReadonlySignal<boolean> | ComputedSignal<boolean>
FormFieldCallbacksonAssociated?,onDisabled?,onReset?,onStateRestore?
FormFieldHandleinternals,setValidity,setCustomValidity,checkValidity,reportValidity
Controls APIs
Import from @vielzeug/craftit/controls:
createListControl(options)- returns result-based navigation (
ListControlResultwithreason,moved,wrapped,index)
- returns result-based navigation (
createOverlayControl(options)- reason-aware overlay transitions via
setOpen(next, { reason }),onOpen(reason), andonClose(reason)
- reason-aware overlay transitions via
createTextFieldControl(options)- text-field controller with stable ids, validation hooks, and integrated assistive state
createChoiceFieldControl(options)- single/multi choice controller for select, combobox, and grouped checkboxes
createCheckableFieldControl(options)- checkbox/radio/switch helper that bundles checkable state, a11y wiring, and press handling
createA11yControl(host, config)- low-level DOM ARIA/label/helper wiring for advanced custom widgets
Controls contract notes
aria(...)semantics apply broadly:false,null, andundefinedremove ARIA attributes.createOverlayControlclose/open reasons are part of the public contract and intended for typed event payloads.
Observer APIs are exported from @vielzeug/craftit/observers:
resizeObserver(el): ReadonlySignal<{ width: number; height: number }>intersectionObserver(el, options?): ReadonlySignal<IntersectionObserverEntry | null>mediaObserver(query): ReadonlySignal<boolean>
Utility APIs
createId(prefix?)
Also exported:
ref,refs- internal types including
HTMLResult,Directive,Ref,Refs,RefCallback
Directive APIs
each(source, options)
options.renderrenders each item.- Static array source:
options.keyoptional. - Reactive source (
Signal<T[]>or() => T[]):options.keyrequired. options.fallbackrenders when the list is empty.options.selectfilters items before rendering.
For dynamic lists with interactions, prefer event delegation on a stable parent element.
Exports:
attrsbindchooseclasseseachmemoonrawspreadstyleuntilwhen
Common signatures:
when({ condition, then, else })
until(promise, pendingFn?, onError?)
each(source, { render, key, fallback, select })
choose({ value, cases, fallback })
bind({ value, as, event, parse })
memo({ deps, render })until(...) renders Error: <reason> by default when the promise rejects and onError is omitted.
attrs(map)— shorthand for batching DOM property bindings in spread position. Despite the name, entries map to.propertybindings internally.bind({ value })— two-way shorthand built on the same property-binding path used byattrs(...),spread({ '.value': ... }), and template.value/.checkedbindings.
Testing APIs
Import from @vielzeug/craftit/testing.
Primary exports:
mount(...)flush()within(element)fire(event helpers)user(interaction helpers)waitFor(...)waitForEvent(...)mock(tagName, template?)cleanup()install(afterEachHook)
Core test types:
Fixture<T extends HTMLElement>MountOptionsQueryScopeWaitOptions
mount(...) accepts:
- a registered tag name
- an inline
setup(ctx) => templatefunction - an inline
component-style options object withouttag
Stateit Re-Exports
Craftit re-exports @vielzeug/stateit from its main entrypoint. Use Craftit imports directly when building components, or import from @vielzeug/stateit for state-only modules.
import { signal, computed, batch, untrack, readonly } from '@vielzeug/craftit';