SpikiJS v1.0
Documentation

SpikiJS Framework

SpikiJS is an ultra-lightweight (~5KB), high-performance JavaScript framework designed for building reactive user interfaces directly in HTML without complex build tooling or transpilation steps.

~5KB Footprint
Zero dependencies, loads instantaneously in any modern browser.
CSP Compliant
No unsafe inline eval/expressions. Clean separation of template & logic.
Deep Reactivity
ES6 Proxy powered auto-tracking for nested objects and arrays.
CSP Compliance & Zero Inline Logic Rule

SpikiJS enforces strict separation of concerns to comply with strict Content Security Policy (CSP). No eval() or unsafe inline code execution is used.

  • No Inline JS Code: HTML attributes cannot contain JS code, expressions, or operators (e.g. s-click="count++" is invalid). Allowed Exception: Boolean directives (s-if, :hidden, :disabled, :checked) accept state property names directly or with logical NOT ! (e.g. :hidden="!show").
  • No Attribute Parameters: Function parameters are not supported inside HTML attributes (e.g. s-click="save(item)" is invalid). Bind only the method name: s-click="save". Access this.item (or this.child, this.index in loops) directly inside the method!
  • No Dot Modifiers: Attribute names do NOT support dots or modifiers (e.g. s-submit.prevent="submitForm" is invalid). Write clean directive names: s-submit="submitForm" and call e.preventDefault() inside your method if needed.
  • State Access via this: Access and mutate variables directly inside JS methods via this.variableName.

Installation

1. Script Tag / CDN

Include the UMD script directly. Spiki will automatically bind to window.spiki.

<script src="https://unpkg.com/spikijs"></script>

2. ES Module / NPM

Install via NPM for modern bundlers (Vite, Webpack, Rollup).

npm install spikijs
import spiki from 'spikijs';

Quick Start

Define components with s-data="name", register state logic using spiki.data(), and start the engine.

Live Interactive Demo Active
Count:
// HTML
<div s-data="counterApp">
    <button s-click="decrement">-1</button>
    <span s-text="count"></span>
    <button s-click="increment">+1</button>
</div>
// JavaScript
spiki.data('counterApp', () => ({
    count: 0,
    increment() { this.count++; },
    decrement() { this.count--; }
}));
spiki.start();

Directives Reference Summary

Directive Description Example
s-data Defines component instance scope <div s-data="myApp">
s-text Sets safe text content <span s-text="username">
s-html Sets inner HTML content <div s-html="htmlString">
:[attr] Binds dynamic HTML attribute <img :src="avatarUrl">
:class Toggles dynamic classes (HTML & SVG safe) <div :class="classMap">
s-click Attaches click event listener to element <button s-click="save">
s-[event] Attaches native event listener (s-input, s-keyup, etc.) <input s-input="onInput">
s-model Two-way data binding for form inputs <input s-model="query">
s-if Conditionally mounts/removes DOM (supports ! negation) <p s-if="isOnline"> / <p s-if="!isOnline">
:hidden Toggles visibility via native attribute (s-show replacement, supports !) <div :hidden="!show">
s-for Loops array inside <template> <template s-for="item in items">
spiki.compose() Merges modular state slices/factories into a single reactive scope spiki.compose(modA, modB)

s-data Component Scope

s-data="componentName" marks an element as the root of a Spiki component. State returned by the component factory function becomes reactive.

spiki.data('myApp', () => ({
    title: 'Hello Spiki',
    user: { name: 'Alice' }
}));

s-text & s-html

Use s-text for safe text insertion and s-html for rendering raw HTML. Functions passed to s-text receive the element instance el as the first parameter.

s-text (Plain string):
s-text (Computed with el argument):
s-html (Formatted markup):
spiki.data('textDemo', () => ({
    message: 'Reactive Text Content',
    richHtml: '<span class="text-emerald-500 font-bold">Safe HTML Content</span>',
    upperMessage(el) {
        return this.message.toUpperCase();
    }
}));

:[attr] & :class

Prefix any standard HTML or SVG attribute with : for dynamic binding. :class accepts string class names or objects with boolean values, fully safe for both HTML and SVG elements.

HTML Element Status:
SVG Element with reactive class binding
// HTML & SVG binding
<svg :class="svgClass" viewBox="0 0 24 24">...</svg>

spiki.data('attrDemo', () => ({
    isActive: false,
    toggleActive() { this.isActive = !this.isActive; },
    get activeClass() {
        return this.isActive ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-700';
    },
    get svgClass() {
        return this.isActive ? 'stroke-indigo-500 text-indigo-500' : 'stroke-slate-400 text-slate-400';
    }
}));

s-click & s-[event] Event Handling

Attach standard DOM event listeners using s-click, s-input, s-submit, s-keyup, etc. Event handlers receive the native Event object e without inline arguments.

Last Event Log:
spiki.data('eventDemo', () => ({
    lastLog: 'None',
    handleClick(e) {
        this.lastLog = 'Clicked button at x: ' + e.clientX;
    },
    handleKeyup(e) {
        if (e.key === 'Enter') this.lastLog = 'Enter pressed!';
        else this.lastLog = 'Key pressed: ' + e.key;
    }
}));

s-model & s-value

s-model creates two-way bindings for inputs, textareas, checkboxes, and radios. s-value provides one-way value binding.

State: { name: "", isSubscribed: }

s-if Conditional Mounting

s-if="condition" conditionally mounts or completely unmounts DOM nodes from the tree based on boolean evaluation. Supports negation using ! prefix.

✓ s-if="show" (Mounted in DOM tree)
✕ s-if="!show" (Negation: Mounted when false)
<!-- Mounted when show is true -->
<div s-if="show">Mounted when true</div>

<!-- Mounted when show is false (negation) -->
<div s-if="!show">Mounted when false</div>

:hidden Dynamic Visibility (s-show Alternative)

Spiki uses native HTML attribute binding :hidden="!condition" as a high-performance alternative to s-show. The element remains in the DOM tree while toggling display visibility. Fully supports ! negation.

:hidden="!isVisible" (Visible when isVisible is true)
:hidden="isVisible" (Visible when isVisible is false)
// Toggles native HTML 'hidden' attribute without DOM unmounting
<!-- Visible when isVisible is true -->
<div :hidden="!isVisible">Visible when true</div>

<!-- Visible when isVisible is false (negation) -->
<div :hidden="isVisible">Visible when false</div>
Pro Tip: Prevent Initial Render Glitch (FOUC)

Add the static hidden attribute to your HTML element if it should be hidden on initial page load (e.g. <div hidden :hidden="show">). This prevents element flickering before JavaScript loads.

s-for & s-key

Render lists by placing s-for="item in array" on a <template> element. Always supply a unique key with s-key="id" for optimal DOM reuse. Inside loop event handlers, Spiki automatically provides this.item.

<template s-for="(fruit, idx) in fruits" s-key="id">
    <li>
        <span s-text="fruit.name"></span>
        <!-- Notice: No arguments passed to removeItem! -->
        <button s-click="removeItem">Remove</button>
    </li>
</template>

spiki.data('listDemo', () => ({
    fruits: [ { id: 1, name: 'Apple' }, { id: 2, name: 'Banana' } ],
    removeItem() {
        // 'this.fruit' and 'this.idx' are automatically injected into 'this'!
        this.fruits.splice(this.idx, 1);
    }
}));
Automatic Loop Scope Injection (this.child, this.index)

Inside a loop like <template s-for="(child, index) in children">, event handlers like s-click="removeItem" do NOT require arguments. Spiki automatically injects this.child (current item) and this.index (current index) directly into this when the handler runs!

s-init & s-destroy

s-init="onMount" executes immediately when the component mounts. s-destroy="onUnmount" runs when the component is unmounted from DOM tree to prevent memory leaks. Both functions receive the DOM element el as parameter.

Mount Status:
<div s-data="userApp" s-init="onMount" s-destroy="onUnmount">
    <span s-text="status"></span>
</div>

spiki.data('userApp', () => ({
    status: 'Booting...',
    onMount(el) {
        console.log('Component mounted on element:', el);
        this.status = 'Ready';
    },
    onUnmount(el) {
        console.log('Cleaning up resources...');
    }
}));

s-effect Reactive Side Effects

s-effect="handler" registers an automatic reactive watcher function. Spiki automatically tracks all reactive state properties accessed inside the handler function and re-runs it whenever those properties mutate!

Synced Value:
// s-effect automatically re-executes whenever this.note mutates!
<div s-data="noteApp" s-effect="autoSave">
    <textarea s-model="note"></textarea>
</div>

spiki.data('noteApp', () => ({
    note: '',
    autoSave() {
        // Read reactive state (this.note) and save to external storage
        localStorage.setItem('user_note', this.note);
    }
}));
Warning: Avoid Infinite Loops in s-effect

Do NOT mutate component state properties (e.g. this.count++) inside an s-effect function! Modifying state read inside an effect re-triggers the watcher endlessly, causing an infinite loop. Use s-effect strictly for side effects like localStorage, DOM calls, or external network requests.

Instance API & DOM References

Inside any component method, this grants direct access to state properties as well as built-in Spiki instance helpers.

this.$refs

Dictionary of DOM elements marked with s-ref="key". Grants direct access to raw HTML elements without querySelector.

this.$root

Reference to the root element containing s-data. Useful for class toggles, container dimensions, or custom events.

this.$store

Shared global reactive store. Any property accessed via this.$store automatically triggers component updates when changed.

spiki.raw(proxy)

Unwraps a Spiki Proxy object back into a plain JavaScript object. Essential for JSON.stringify, logging, or API calls.

spiki.compose(...modules)

Merges multiple modular state slices or factories into a single unified reactive scope while preserving native getters and setters.

1 DOM References with s-ref

Use s-ref="name" to register direct DOM element references inside this.$refs. Ideal for focusing inputs, reading canvas contexts, controlling video elements, or integrating 3rd-party libraries.

Native Element Tag:
<input s-ref="textInput" type="text">
<button s-click="focusInput">Focus</button>

spiki.data('refDemo', () => ({
    focusInput() {
        // Access raw HTMLInputElement directly
        this.$refs.textInput.focus();
    },
    selectText() {
        this.$refs.textInput.select();
    }
}));

2 Root Element Reference with this.$root

this.$root points to the component's root container element (the HTML tag bearing s-data).

Root Tag:
spiki.data('rootDemo', () => ({
    highlightRoot() {
        // Manipulate root container element directly
        this.$root.classList.toggle('ring-2');
        this.$root.classList.toggle('ring-indigo-500');
    }
}));

3 Global Store spiki.store()

Register a shared global store using spiki.store('name', stateObject). Any component accessing this.$store.name automatically subscribes to store state updates.

Automatic init() Store Execution

If your store object defines an init() method, Spiki automatically executes store.init.call(store) immediately upon store registration! This is ideal for loading persisted state from localStorage or initializing listeners.

Store Theme:
Initialized Status:
// 1. Register store with automatic init()
spiki.store('settings', {
    theme: 'dark',
    initialized: false,
    init() {
        // Automatically runs when store is registered!
        this.theme = localStorage.getItem('theme') || 'dark';
        this.initialized = true;
    }
});
// 2. Read store inside any component
spiki.data('myComponent', () => ({
    get currentTheme() {
        return this.$store.settings.theme; // Reactive state access!
    }
}));

4 Unwrapping Reactive Proxies with spiki.raw()

Spiki wraps state objects in ES6 Proxies for reactive tracking. Use spiki.raw(proxy) to unwrap a proxy object back into a plain, non-reactive JavaScript object or array.

// Unwrap proxy before console.log, JSON.stringify, or API fetch
spiki.data('userForm', () => ({
    user: { name: 'Alice', email: 'alice@example.com' },

    saveUser() {
        // Extract raw un-proxied JS object
        const rawPayload = spiki.raw(this.user);

        console.log("Raw Object:", rawPayload);
        fetch('/api/user', {
            method: 'POST',
            body: JSON.stringify(rawPayload)
        });
    }
}));

5 Modular State Composition with spiki.compose()

Split large component logic into reusable modules or factories. spiki.compose() merges them into a single reactive component while preserving native getters and setters. All methods share access to the same reactive proxy via this.

Unified State: Count = | Mode =
// 1. Separate state slices / modular factories
const createCounterSlice = () => ({
    count: 0,
    increment() { this.count++; }
});

const createThemeSlice = () => ({
    darkMode: false,
    get themeMode() { return this.darkMode ? 'Dark' : 'Light'; },
    toggleTheme() { this.darkMode = !this.darkMode; }
});
// 2. Compose slices inside spiki.data()
spiki.data('composeDemo', () =>
    spiki.compose(
        createCounterSlice,
        createThemeSlice,
        {
            init() {
                console.log('Composed component initialized!');
            }
        }
    )
);