Components
Weblisk components are standard Custom Elements with a thin signal bridge. No Shadow DOM. No template compiler. No build step.
import { component } from 'weblisk';
Also available as a deep import: import { component } from 'weblisk/ui/component.js';
Defining a Component
component('wl-counter', { props: { start: 0 }, setup({ props, $, effect }) { effect(() => { $('.count').textContent = props.start(); }); $('.inc').onclick = () => props.start.set(props.start() + 1); $('.dec').onclick = () => props.start.set(props.start() - 1); } });
Usage — plain HTML:
<wl-counter start="5"> <button class="dec">-</button> <span class="count">5</span> <button class="inc">+</button> </wl-counter>
The HTML works before JavaScript loads — the count shows "5", buttons are visible. When the component upgrades, it adds interactivity. This is progressive enhancement.
Props
Props are declared with default values. The type of the default determines how attributes are parsed:
component('wl-widget', { props: { label: '', // String — attribute value as-is count: 0, // Number — parsed with Number() active: false, // Boolean — "false" and "0" are false, everything else is true }, setup({ props }) { typeof props.label() // 'string' typeof props.count() // 'number' typeof props.active() // 'boolean' } });
Each prop is a signal — call it to read, call .set() to write:
props.count() // read: 0 props.count.set(5) // write: 5 props.count.set(n => n + 1) // functional update: 6
When an HTML attribute changes (e.g. via setAttribute), the prop signal updates automatically.
Setup Context
The setup function receives a context object:
| Property | Type | Description |
|----------|------|-------------|
| props | Object | Signal getters for each declared prop |
| el | HTMLElement | The custom element instance |
| $ | (selector) => Element | querySelector scoped to this element |
| $$ | (selector) => Element[] | querySelectorAll scoped to this element
|
| effect | (fn) => dispose | Auto-tracked effect, disposed on disconnect |
| emit | (name, detail?) => void | Dispatch a bubbling CustomEvent |
| cleanup | (fn) => void | Register a function to call on disconnect |
Effects
Effects created via the context are automatically disposed when the element disconnects from the DOM:
component('wl-clock', { setup({ $, effect, cleanup }) { const tick = setInterval(() => { $('.time').textContent = new Date().toLocaleTimeString(); }, 1000); cleanup(() => clearInterval(tick)); } });
Events
Use emit to dispatch standard CustomEvents that bubble:
component('wl-color-picker', { props: { value: '#000000' }, setup({ props, $, emit }) { $('input').oninput = (e) => { props.value.set(e.target.value); emit('color-change', { color: e.target.value }); }; } });
Listen from any parent:
<wl-color-picker value="#6366f1"> <input type="color" /> </wl-color-picker> <script type="module"> document.querySelector('wl-color-picker').addEventListener('color-change', (e) => { document.body.style.setProperty('--accent', e.detail.color); }); </script>
Template Fallback
If the element is empty, an optional template function provides initial DOM:
component('wl-spinner', { template() { return '<div class="spinner" role="status" aria-label="Loading"></div>'; }, setup({ el }) { // Spinner is already visible — no further setup needed } });
If the element already has children (server-rendered), the template is skipped. This ensures components work with any server template engine.
Working With Any Framework
Components are standard Custom Elements — they work anywhere HTML works:
Rails (ERB)
<wl-toggle active="<%= @feature.enabled? %>"> <button><%= @feature.name %></button> </wl-toggle>
Django (Jinja2)
<wl-toggle active="{{ feature.enabled|lower }}"> <button>{{ feature.name }}</button> </wl-toggle>
Go (html/template)
<wl-toggle active="{{.Enabled}}"> <button>{{.Name}}</button> </wl-toggle>
Static HTML
<wl-toggle active="true"> <button>Dark Mode</button> </wl-toggle>
All identical. The server renders the HTML. The component enhances it.
Component vs. Island
Both enhance existing HTML. The difference:
| | component() | enhance() |
|---|---|---|
| Registers a Custom Element | Yes | No |
| Reusable with attributes | Yes | No (selector-based) |
| Works in any template engine | Yes | Yes |
| Auto-cleanup on disconnect | Yes | Manual |
| When to use | Reusable interactive widgets | One-off page-specific behavior |
Use component() for reusable widgets. Use enhance() for page-specific behavior.