A client-side JavaScript framework
designed for swift web development with minimum JavaScript

Signalize leverages modern ES modules and import maps.
It imports only a small 2 KB core and you decide what to import and when.
This makes the framework small, scalable and flexible.

Installation Guides

Signalize can be used anywhere.

Ecosystem

Signalize functions are split into small modules and you decide what to import and when.

Signals & Reactivity

Bind signals and values to element attributes and properties.

Directives

Use directives to make your website interactive in no time.

Web Components

Friendly API for Native Web Components.

SPA

Create a single page application from any website easily.

Dialogs

Open, close and manipulate native dialog elements with dialog utilities.

Logger

Send JavaScript errors to server without any effort.

Web Perf

Utilities that will help you easily optimize UX.

Try Signalize in the Playground

<input id="input" placeholder="Type something...">
<div id="output"></div>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/scope": "https://cdn.jsdelivr.net/npm/signalizejs/scope/+esm",
    "signalizejs/signal": "https://cdn.jsdelivr.net/npm/signalizejs/signal/+esm",
    "signalizejs/bind": "https://cdn.jsdelivr.net/npm/signalizejs/bind/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    // Create a Signalize instance
    const { resolve } = new Signalize();
    const { bind, signal } = await resolve('signal', 'bind');

    // Create a signal
    const text = signal('');
    // Bind the signal to the input value attribute
    bind(document.querySelector('#input'), {value: text});
    // Bind the signal to the output element to innerHTML
    bind(document.querySelector('#output'), { text });

    // Watch the signal for changes
    text.watch(({ newValue, oldValue }) => {
        console.log('New value:', newValue);
        console.log('Old value:', oldValue);
    });
    // Watch signal before it is set
    // This way we can perform checks, validation,
    // modify values or stop the setter completely
    text.watch(({ newValue, oldValue }) => {
        /*
            Uncomment the return, if you want to trigger
            a signal modifier before the signal value is set.
        */
        /*
        return {
            settable: newValue.length < 10,
            value: newValue.toUpperCase()
        }
        */
    }, { execution: 'beforeSet' });
</script>

Signals are reactive primitive and they are the heart of reactivity in Signalize.
To easily bind signals to elements, there is a bind method, that provides two-directional data binding and automatically converts strings to numbers in inputs when possible.
Signals are defined as a class, so you can easily extend them for additional functionality.

<text-field>
    <input ref="text" required>
    <button ref="btn">Submit first</button>
    <div ref="output"></div>
</text-field>
<br>
<text-field>
    <input ref="text" required>
    <button ref="btn">Submit second</button>
    <div ref="output"></div>
</text-field>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/scope": "https://cdn.jsdelivr.net/npm/signalizejs/scope/+esm",
    "signalizejs/signal": "https://cdn.jsdelivr.net/npm/signalizejs/signal/+esm",
    "signalizejs/strings/cases": "https://cdn.jsdelivr.net/npm/signalizejs/strings/cases/+esm",
    "signalizejs/bind": "https://cdn.jsdelivr.net/npm/signalizejs/bind/+esm",
    "signalizejs/component": "https://cdn.jsdelivr.net/npm/signalizejs/component/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    // Create a Signalize instance
    const { resolve } = new Signalize();

    const { component, bind, signal } = await resolve('component', 'bind', 'signal');
    // Define text-field web component
    component('text-field', {
        props: { /* key: default value */ },
        setup({ $el, $refs }) {
            // Init signal
            const text = signal('');

            // Bind signal
            // Use ref() to get direct component child element
            bind($refs.text, { value: text });
            bind($refs.output, { html: text });

            // Handle form submit
            $refs.btn.onclick = (event) => {
                event.preventDefault();
                alert(text());
            }

            // Return public data
            return { /* key: value */ }
        }
    });
</script>

Component plugin provide an easy way to define reusable web component.

<words-list>
    <form @submit="submit">
        <!-- Shortcuts for attribute binding -->
        <input {value} required>
        <button>Add</button>
        <br>
        <!-- Native for of/in loops -->
        <template :for="item of items">
            <!-- Iterator feature: count, first, last, odd, even -->
            <span
                :text="item + (iterator().last ? '' : ', ')"
                :style="iterator().odd ? 'color:red': 'color:blue'"
            ></span>
        </template>
    </form>
</words-list>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/bind": "https://cdn.jsdelivr.net/npm/signalizejs/bind/+esm",
    "signalizejs/directives": "https://cdn.jsdelivr.net/npm/signalizejs/directives/+esm",
    "signalizejs/directives/for": "https://cdn.jsdelivr.net/npm/signalizejs/directives/for/+esm",
    "signalizejs/directives/if": "https://cdn.jsdelivr.net/npm/signalizejs/directives/if/+esm",
    "signalizejs/dom/traverser": "https://cdn.jsdelivr.net/npm/signalizejs/dom/traverser/+esm",
    "signalizejs/evaluator": "https://cdn.jsdelivr.net/npm/signalizejs/evaluator/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/scope": "https://cdn.jsdelivr.net/npm/signalizejs/scope/+esm",
    "signalizejs/signal": "https://cdn.jsdelivr.net/npm/signalizejs/signal/+esm",
    "signalizejs/strings/cases": "https://cdn.jsdelivr.net/npm/signalizejs/strings/cases/+esm",
    "signalizejs/component": "https://cdn.jsdelivr.net/npm/signalizejs/component/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    const { resolve } = new Signalize();

    // Use the directives plugin
    const { component, signal } = await resolve('directives', 'signal', 'component');

    component('words-list', () => {
        const value = signal('');
        const items = signal([]);

        return {
            submit: (event) => {
                event.preventDefault();
                items([...items(), value().trim()]);
                value('');
            },
            items,
            value
        }
    });
</script>

Directives plugin provides a simple way to bind logic to elements. It's similar like in Vue or Alpine, but with a few diferences like that under the hood it uses Signals, root element is not necessary and data can be defined on the same element where they are used.

<custom-list>
    <button @click="count(count() + 1)">+<span :text="count">3</span></button>
    <button @click="count(count() - 1)">-<span :text="count">3</span></button>
    <ul>
        <template :for="i of count">
            <li :text="'Rendered - ' + i"></li>
        </template>
        <!-- prerendered -->
            <li>Prerendered - 0</li>
            <li>Prerendered - 1</li>
            <li>Prerendered - 2</li>
        <!-- /prerendered -->
    </ul>
</custom-list>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/bind": "https://cdn.jsdelivr.net/npm/signalizejs/bind/+esm",
    "signalizejs/directives": "https://cdn.jsdelivr.net/npm/signalizejs/directives/+esm",
    "signalizejs/directives/for": "https://cdn.jsdelivr.net/npm/signalizejs/directives/for/+esm",
    "signalizejs/directives/if": "https://cdn.jsdelivr.net/npm/signalizejs/directives/if/+esm",
    "signalizejs/dom/traverser": "https://cdn.jsdelivr.net/npm/signalizejs/dom/traverser/+esm",
    "signalizejs/evaluator": "https://cdn.jsdelivr.net/npm/signalizejs/evaluator/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/scope": "https://cdn.jsdelivr.net/npm/signalizejs/scope/+esm",
    "signalizejs/signal": "https://cdn.jsdelivr.net/npm/signalizejs/signal/+esm",
    "signalizejs/strings/cases": "https://cdn.jsdelivr.net/npm/signalizejs/strings/cases/+esm",
    "signalizejs/component": "https://cdn.jsdelivr.net/npm/signalizejs/component/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    const { resolve } = new Signalize();
    // Use the directives plugin
    const { component, signal } = await resolve('directives', 'component', 'signal');
    component('custom-list', () => ({
        count: signal(3)
    }));
</script>

Directives such as for and if can work with prerendered state from the server. If these directives detects prerendered section after them, they will render only after some signal changes instead of on init. This way you don't have to add a loader or hide elements that were not rendered yet.

<a href="/path">Link</a>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/ajax": "https://cdn.jsdelivr.net/npm/signalizejs/ajax/+esm",
    "signalizejs/dom/ready": "https://cdn.jsdelivr.net/npm/signalizejs/dom/ready/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/snippets": "https://cdn.jsdelivr.net/npm/signalizejs/snippets/+esm",
    "signalizejs/spa": "https://cdn.jsdelivr.net/npm/signalizejs/spa/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs'

    const { resolve } = new Signalize();
    const { on, navigate } = await resolve('spa', 'event');

    // Or send custom visit request
    navigate({
        url: '/path',
        // Optional
        scrollX: 0,
        scrollY: 0,
        // 'push' or 'replace'
        stateAction: 'push'
    })

    on('spa:navigation:start', (data) => { // Visit start });
    on('spa:navigation:end', (data) => { // Visit end });
</script>

SPA plugin can turn any website into a single page application. Under the hood it uses View Transition when possible.

<div snippet="snippet-1">Hello ...</div>
<div snippet="snippet-2"></div>

<button id="button">Redraw</button>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/snippets": "https://cdn.jsdelivr.net/npm/signalizejs/snippets/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';

    const { resolve } = new Signalize();
    const { on, redrawSnippet } = await resolve('snippets', 'event');

    on('click', '#button', () => {
        // Redraw snippets based on HTML input
        redrawSnippet(`
            <div snippet="snippet-1">Hello World!</div>
            <div snippet="snippet-2">Clicked</div>
        `)
    });
</script>

Snippets plugin simplifies HTML snippets synchronization for example from response with the existing HTML. This allows you to append products to category, load additional carousel items, redraw dashboard and etc.

<dialog dialog="hello-world">
    Hello World!<br><br>
    <button dialog-close="hello-world">Close</button>
</dialog>

<button dialog-open="hello-world">Open Dialog</button>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/dom/ready": "https://cdn.jsdelivr.net/npm/signalizejs/dom/ready/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/dialog": "https://cdn.jsdelivr.net/npm/signalizejs/dialog/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    const { resolve } = new Signalize();

    await resolve('dialog');
</script>

Dialog plugin simplifies the usage of the native dialog element.

<div id="app"></div>

<script type="importmap">{
  "imports": {
    "signalizejs": "https://cdn.jsdelivr.net/npm/signalizejs/+esm",
    "signalizejs/bind": "https://cdn.jsdelivr.net/npm/signalizejs/bind/+esm",
    "signalizejs/event": "https://cdn.jsdelivr.net/npm/signalizejs/event/+esm",
    "signalizejs/mutation-observer": "https://cdn.jsdelivr.net/npm/signalizejs/mutation-observer/+esm",
    "signalizejs/scope": "https://cdn.jsdelivr.net/npm/signalizejs/scope/+esm",
    "signalizejs/signal": "https://cdn.jsdelivr.net/npm/signalizejs/signal/+esm",
    "signalizejs/hyperscript": "https://cdn.jsdelivr.net/npm/signalizejs/hyperscript/+esm"
  }
}</script>

<script type="module">
    import Signalize from 'signalizejs';
    const { resolve } = new Signalize();

    const { h, signal } = await resolve('hyperscript', 'signal');
    // Create text signal
    const text = signal('Change me!');

    document.querySelector('#app').replaceWith(
        // Create elements
        h('div', { id: 'app' },
            // Bind text signal to element attributes
            h('input', { value: text }),
            h('div', text)
        )
    );
</script>

Hyperscript plugin is a simple way to define reusable and reactive template or elements. Internally it automatically binds Signals to attributes and properties.

Showcase

Simplicity to the win

Signalize is a client-side, module-based, platform-focused, dependency-less JavaScript framework. It is inspired by frameworks such as Vue.js (Directives), Svelte (Data Binding), Angular & Solid (Signals), Nette (Snippets), Hotwired Turbo (SPA), and jQuery (Utilities).

By default, Signalize contains only an ES modules loader. All additional functionality is split into modules, allowing you to decide what to import and when. This makes the framework small and flexible.

Signalize serves as an alternative for situations where you need features similar to those of modern frameworks, prefer something small and simple, or have a backend written in a language other than JavaScript.

Vladimír Macháček
Author of Signalize