@guifendjy/shadow-realm
v2.1.5
Published
A lightweight, reactive DOM framework built around a Signal-based reactivity system and a directive pipeline. Bind HTML attributes to JavaScript state — the DOM updates automatically whenever state changes. No virtual DOM, no build step required. Inspired
Maintainers
Readme
Shadow Realm
A lightweight, reactive DOM framework built around a Signal-based reactivity system and a directive pipeline. Bind HTML attributes to JavaScript state — the DOM updates automatically whenever state changes. No virtual DOM and no build step required. Inspired by Alpine.js.
Installation
npm install @guifendjy/shadow-realmimport Realm, { Shadow } from "@guifendjy/shadow-realm";Quick Start
<div s-state="{ count: 0 }">
<span s-text="count"></span>
<button on:click="count++">Increment</button>
</div>
<script type="module">
import Realm from "@guifendjy/shadow-realm";
new Realm(document.getElementById("app")).initialize();
</script>Table of Contents
Realm
The main reactive controller for a DOM subtree.
new Realm(root?, context?)| Parameter | Type | Default | Description |
| --------- | ---------------- | --------------- | -------------------------------------------- |
| root | HTMLElement | document.body | Root element to make reactive |
| context | Object \| null | null | Parent context to inherit from another Realm |
Methods
realm.initialize() → Realm
Activates all bindings, attaches event listeners, and runs effects. Safe to call multiple times — subsequent calls are no-ops.
const realm = new Realm(document.querySelector("#app"));
realm.initialize();realm.destroy()
Tears down the Realm: removes event listeners, unsubscribes all signal bindings, and runs effect cleanup functions. Call this before removing the element from the DOM.
realm.destroy();Properties
| Property | Type | Description |
| --------------- | ---------------- | -------------------------------------- |
| realm.root | HTMLElement | The root DOM element |
| realm.context | Object \| null | The injected parent context |
| realm.ready | boolean | Whether initialize() has been called |
Shadow Registry
The Shadow export is the global registry for directives, plugins, and stores. It is shared across all Realm instances.
import { Shadow } from "@guifendjy/shadow-realm";Shadow.directive(name, handler)
Registers a custom reactive directive. The name must start with "s-".
Shadow.directive("s-tooltip", ({ el, expression, execute }) => {
el.title = execute(expression);
});Handler arguments:
| Property | Description |
| --------------- | -------------------------------------------------------- |
| el | The DOM element the directive is on |
| expression | The raw attribute value string |
| value | The updated signal value (when a reactive token changed) |
| context | The context the element is in. |
| execute(expr) | Evaluates an expression string in the element's scope |
Using extendContext The extendContext method is a pure function: it does not modify the original context. Instead, it returns a new context object that includes your additional data. This is used specifically when you want to use the result of the expression within the template of the targeted node.
R.directive("s-**", ({ el, expression, execute, context }) => {
// 1. Evaluate the expression provided in s-**="expression"
const result = execute(expression);
// 2. Create a NEW context that includes this result.
// We extend the context so the child nodes can access '$match'
const branchedContext = context.extendContext({ $match: result });
// 3. Initialize the Realm with the extended context
// The child template can now use s-text="$match"
const realm = new Realm(node, branchedContext);
});Shadow.use(pluginFn) → Shadow
Registers a plugin. Chainable.
Shadow.use(MyPlugin).use(AnotherPlugin);Shadow.store(name, callback)
Registers a global reactive store. See Stores.
Shadow.state(name, callback)
Registers a named, reusable state factory.
Shadow.state("counter", (start = 0) => ({ count: start }));Declaring State
Use the s-state attribute to declare a reactive scope on any element. The value is a plain JavaScript object expression. It can include an init method that will be executed during the Realm's initialization.
<div s-state="{ name: 'Alice', count: 0, init(){...} }">
<span s-text="name"></span>
</div>Nested State
Child elements can declare their own s-state. They automatically inherit access to all ancestor state.
<div s-state="{ user: 'Alice' }">
<div s-state="{ role: 'admin' }">
<!-- Both `user` and `role` are accessible here -->
<span s-text="user + ' — ' + role"></span>
</div>
</div>Reactivity and State Updates
When a state or store is defined via s-state, Shadow.state(...), or Shadow.store(...), the engine creates a reactive scope and converts every property into a Signal instance. That means the DOM is updated by signal notifications, not by mutating plain object/array properties in place.
s-state="{ count: 0 }"becomes an internal scope like{ count: SignalInstance }- If a directive reads
count, it subscribes to that exactcountsignal
Example:
// no DOM update
state.user.name = "Bob";
// triggers DOM update
state.user = { ...state.user, name: "Bob" };Because the engine tracks dependency access through proxies, it knows exactly which signals each directive depends on. When a signal changes, it notifies only those subscribers and the DOM updates automatically. In the current implementation, state mutation must happen by replacing the signal value rather than modifying nested data in place. A future proxy layer may allow direct mutation to be translated into signal updates.
Directives
All directive attributes are removed from the DOM after processing.
s-text
Sets the textContent of an element.
<span s-text="name"></span> <span s-text="count + ' items'"></span>s-value
Sets the value property of a form element.
<input s-value="searchQuery" />s-show
Toggles visibility. Preserves the element's original display value and keeps aria-hidden in sync.
s-switch
The s-switch directive allows for conditional rendering of elements based on a matching expression. It functions similarly to a JavaScript switch statement, rendering only the branch that matches the provided value.
Usage: The directive must be placed on a tag. Inside that template, you define different branches using the case attribute. You can also define a fallback using the default attribute.
<div s-state="{ active: 0 }">
<template s-switch="active">
<div case="0">
<!-- This content renders when active == 0 -->
<button
s-state="{ count: 0 }"
on:click="count++"
s-text="'Count is: ' + count"></button>
</div>
<div case="1">
<p>Case 1 is active.</p>
</div>
<div case="2">
<p>Case 2 is active.</p>
</div>
<div default>
<p>This appears if active doesn't match 0, 1, or 2.</p>
</div>
</template>
<button on:click="active++">Next Case</button>
</div><div s-show="isLoggedIn">Welcome!</div>s-class
Dynamically adds/removes CSS classes. Accepts an object (class → boolean) or a string.
<!-- Object syntax -->
<div s-class="{ active: isActive, disabled: !isEnabled }"></div>
<!-- String syntax -->
<div s-class="currentTheme"></div>s-style
Applies inline styles. Accepts an object (camelCase props) or a CSS string.
<!-- Object syntax -->
<div s-style="{ color: textColor, fontSize: '14px' }"></div>
<!-- String syntax -->
<div s-style="'color: red; font-weight: bold'"></div>s-if
Conditionally mounts and unmounts a <template> element's content. The cloned content is initialized as a child Realm.
<template s-if="showModal">
<div class="modal">...</div>
</template>Must be used on a
<template>element.
s-for
Renders a list from a <template>, using an LCS diff to minimize DOM operations on updates.
<template s-for="item in items">
<div>
<p s-text="item.name"></p>
<p s-text="$index"></p>
</div>
</template>Override $index.
<template s-for="(item, indexName) in items">
<div>
<p s-text="item.name"></p>
<p s-text="indexName"></p>
</div>
</template>Must be used on a
<template>element.
s-effect
Runs an expression once as a side effect when the Realm mounts. The expression may return a cleanup function.
<div s-effect="initChart()"></div>Custom effect directives can be registered using the s-effect-* naming pattern.
s-src
Safely sets the src attribute. Blocks javascript: protocol injection and supports Promises.
<img s-src="avatarUrl" />s-disabled
Sets the disabled boolean attribute.
<button s-disabled="isLoading">Submit</button>s-ref
Registers the element in the global $refs map.
<canvas s-ref="myCanvas"></canvas>s-id
Sets the element's id attribute dynamically.
<section s-id="'section-' + index"></section>s-scroll-text
Animates text changes with a vertical slide transition.
<span s-scroll-text="currentLabel"></span>s-markdown
Renders a Markdown string as HTML. Requires marked to be loaded on the page.
<div s-markdown="markdownContent"></div>Directive Quick Reference
| Attribute | Value | Reactive | Description |
| --------------------- | ----------------- | -------- | -------------------------------------------------------------- |
| s-state | JS object literal | — | Declares reactive state scope |
| s-text | Expression | ✅ | Sets textContent |
| s-value | Expression | ✅ | Sets .value |
| s-show | Expression | ✅ | Toggles visibility |
| s-switch | Expression | ✅ | renders elements based on a matching expression (<template>) |
| s-class | Object or string | ✅ | Adds/removes classes |
| s-style | Object or string | ✅ | Applies inline styles |
| s-if | Expression | ✅ | Conditional render (<template>) |
| s-for | item in list | ✅ | List render (<template>) |
| s-effect | Expression | — | On-mount side effect |
| s-effect-[modifier] | Expression | - | add a modifier to create custom side effect |
| s-src | Expression | ✅ | Sets src safely |
| s-disabled | Expression | ✅ | Sets disabled |
| s-ref | Identifier | — | Registers in $refs |
| s-id | Expression | ✅ | Sets id |
| s-scroll-text | Expression | ✅ | Animated text swap |
| s-markdown | Expression | ✅ | Renders Markdown |
| on:[event] | Expression | — | DOM event listener |
Event Listeners
Attach DOM event listeners with on:[eventName].
<button on:click="count++">Click</button>
<input on:input="query = $event.target.value" />
<form on:submit="handleSubmit()"></form>Special variables available in event expressions:
| Variable | Description |
| --------- | ----------------------------- |
| $event | The raw DOM Event object |
| $target | Shorthand for $event.target |
Helpers in Scope
These are available inside any directive expression or event handler.
| Helper | Description |
| -------------------------- | ------------------------------------------ |
| $uniid(prefix?, length?) | Generates a unique ID |
| $debounce(fn, delay?) | Returns a debounced version of a function |
| $throttle(fn, delay?) | Returns a throttled version of a function |
| $store | Access registered global stores |
| $refs | Access registered element refs |
| $event | Current DOM event (event handlers only) |
| $target | Current event target (event handlers only) |
| $index | Current loop index (inside s-for only) |
Standard globals such as console, Math, Date, JSON, and parseInt are also available.
Stores
Stores provide shared reactive state accessible from any Realm on the page. They support a built-in init method that runs during initialization.
Shadow.store("cart", () => ({
items: [],
total: 0,
init() {
console.log("Cart initialized");
},
}));Shadow.store("cart", () => ({
items: [],
total: 0,
}));State declarations
State declarations provide a way to declare state outside of the template. They also support the built-in init method.
Shadow.state("cart", () => ({
items: [],
total: 0,
}));Use it like this:
<div s-state="cart"></div>Access in templates with $store.storeName.property:
<span s-text="$store.cart.total"></span>
<div s-show="$store.cart.items.length > 0">...</div>Mutate from event handlers:
<button on:click="$store.cart.total = 0">Clear</button>Refs
Refs provide direct access to DOM elements from within expressions.
<input s-ref="emailInput" />
<button on:click="$refs.emailInput.focus()">Focus Email</button>Programmatic registration:
Shadow.$refs("emailInput", document.querySelector("#email"));Plugins
A plugin is a function that receives the Shadow class and registers one or more directives.
function TooltipPlugin(Shadow) {
Shadow.directive("s-tooltip", ({ el, expression, execute }) => {
el.title = execute(expression);
});
}
Shadow.use(TooltipPlugin);Effect directives follow the s-effect-* naming convention and run during the effects phase (on mount):
function UpperCaseEffect(Shadow) {
Shadow.directive("s-effect-upper", ({ el }) => {
el.textContent = el.textContent.toUpperCase();
});
}<p s-effect-upper>hello world</p>
<!-- renders: HELLO WORLD -->Contributing
Found a bug or have a feature suggestion? Feel free to open an issue, submit a pull request, or fork the repository.
License
MIT © 2025 Dads Guifendjy Paul
