Skip to main content

Components

Weblisk components are standard Custom Elements with a thin signal bridge. No Shadow DOM. No template compiler. No build step.

js
import { component } from 'weblisk';

Also available as a deep import: import { component } from 'weblisk/ui/component.js';

Defining a Component

js
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:

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:

js
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:

js
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:

js
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:

js
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:

html
<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:

js
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)

erb
<wl-toggle active="<%= @feature.enabled? %>">
  <button><%= @feature.name %></button>
</wl-toggle>

Django (Jinja2)

html
<wl-toggle active="{{ feature.enabled|lower }}">
  <button>{{ feature.name }}</button>
</wl-toggle>

Go (html/template)

html
<wl-toggle active="{{.Enabled}}">
  <button>{{.Name}}</button>
</wl-toggle>

Static HTML

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.