# wc-mixin A typed mixin factory that removes custom-element boilerplate. ```ts import { WebComponent, disable, enable, number, string, type PropChange } from 'wc-mixin'; const SPEC = { shadow: 'open', // omit for light DOM template: '', css: ':host { display: block; }', events: ['click'], element: { input: 'input.name' }, // refs.input: HTMLInputElement | null elements: { buttons: 'button' }, // refs.buttons: HTMLButtonElement[] props: { userId: number(), // user-id number | undefined min: number({ default: 1 }), // min number ready: enable({ attribute: 'is-ready' }), // is-ready boolean (default false) fetch: disable(), // disable-fetch boolean (default true) }, } as const; class ExampleElement extends WebComponent(SPEC) { override onConnect() { window.addEventListener('resize', () => this.layout(), { signal: this.signal }); } override handleEvent(event: PointerEvent) {} override onPropChange(change: PropChange) { if (change.name === 'min') change.value; // number } layout() {} } customElements.define('example-element', ExampleElement); ExampleElement.observedAttributes; // readonly ('user-id' | 'min' | 'is-ready' | 'disable-fetch')[] ``` - **Props** are stored as values and reflected to attributes; attribute changes decode back. `onPropChange` fires once per real change from either side. - **Attribute names**: an explicit `attribute` is used verbatim; otherwise `kebab(key)`, with `disable-` for `disable()` and `data-` in front when `dataPrefix: true`. - **Codecs**: `string`, `number`, `enable`, `disable`, `choice([...])`, `json()`, `prop({ default, decode, encode })`. - **Shadow DOM**: `shadow` attaches a root and clones `template` into it; `css` is a shared adopted stylesheet. An existing declarative shadow root is used instead (and `shadow`/`template` are ignored). `root` is the shadow root (even when closed) or the element itself. - **Light DOM**: `template` is appended on first connect; `css` is added once per element type, as a `