Complete reference for all 12 directives. Add them as js-name attributes to any HTML element.
js-scope="expression"
Define an inline reactive scope. The expression must evaluate to a plain object. All properties become signals, getters become computeds, and methods are bound to an auto-unwrapping proxy. Child elements of the element inherit these properties. Nested js-scope creates a child scope via prototype inheritance.
<div js-scope="{ count: 0, step: 1 }">
<span js-text="count"></span>
<button js-on:click="count = count + step">+step</button>
<!-- Nested: inherits count and step from parent -->
<div js-scope="{ local: 'nested' }">
<span js-text="'parent count: ' + count"></span>
</div>
</div>
js-scope is processed before other directives on the same element. The element is re-walked with the new scope after the attribute is removed.
js-text="expression"
Set the element's textContent to the result of the expression. Updates reactively whenever a signal it reads changes. Safe — never parses HTML.
<span js-text="count"></span>
<span js-text="'Hello, ' + name + '!'"></span>
<span js-text="items.length + ' items'"></span>
<span js-text="price.toFixed(2)"></span>
js-html="expression"
Set the element's innerHTML to the result of the expression. The HTML is parsed by the browser.
<div js-html="richContent"></div>
js-html with trusted, sanitized content. Never use it with arbitrary user input — doing so is an XSS vulnerability.
js-show="expression"
Toggle the element's visibility by setting style.display = 'none' when the expression is falsy, or removing the inline style when truthy. The element remains in the DOM and its children retain their state.
<p js-show="isLoggedIn">Welcome back!</p>
<div js-show="errors.length > 0" class="alert">
There are errors.
</div>
js-show keeps the element in the DOM (cheaper to toggle frequently); js-if adds/removes it (better when children are expensive to create).
js-if="expression"
Conditionally render an element. When the expression becomes falsy, the element is removed from the DOM and all its effects are cleaned up. When it becomes truthy, a fresh clone of the original template is inserted and walked.
Internally, the element is replaced with a comment anchor (<!-- js-if -->) that tracks the DOM position.
<div js-if="isLoggedIn">
<p>Welcome, <span js-text="username"></span>!</p>
</div>
<div js-if="items.length === 0">
No items yet.
</div>
js-for="item in list"
Render a list of elements. Uses start/end comment anchors to track the region. Each item gets its own child scope containing the item value and $index, both as signals. When the array changes, existing rows are updated in place (signals updated) and rows are added/removed at the end as needed.
<ul>
<li js-for="todo in todos">
<input type="checkbox" js-bind:checked="todo.done" />
<span js-text="todo.text"></span>
<span js-text="'#' + ($index + 1)"></span>
</li>
</ul>
<!-- Access index in event handler -->
<div js-for="item in items">
<button js-on:click="removeItem($index)">Remove</button>
<span js-text="item.name"></span>
</div>
js-bind:attribute="expression"
Reactively bind any HTML attribute or DOM property to an expression. Value rules:
null or false → attribute is removedtrue → attribute is set to empty string (boolean attribute)setAttribute(attr, String(value))checked, disabled, selected, multiple, readonly, indeterminate) → set directly as el[prop] = !!val<a js-bind:href="url">Link</a>
<img js-bind:src="avatarUrl" js-bind:alt="username" />
<input js-bind:disabled="isLoading" />
<input type="checkbox" js-bind:checked="task.done" />
<div js-bind:class="isActive ? 'active highlight' : ''"></div>
<div js-bind:style="'color: ' + textColor"></div>
<button js-bind:aria-pressed="isPressed"></button>
js-on:event[.modifier...]="expression"
Attach a DOM event listener. The expression is evaluated when the event fires. The event object is available as $event.
| Modifier | Behaviour |
|---|---|
| .prevent | Calls event.preventDefault() |
| .stop | Calls event.stopPropagation() |
| .self | Only fires when event.target === el (ignores bubbled events) |
| .once | The listener is removed after the first invocation |
<button js-on:click="count++">Increment</button>
<form js-on:submit.prevent="handleSubmit($event)">...</form>
<div js-on:click.self="closeModal()">...</div>
<button js-on:click.once="trackFirstClick()">...</button>
<!-- Multiple modifiers -->
<a js-on:click.prevent.stop="navigate(url)">Link</a>
js-model="propertyName"
Two-way binding between a form element and a scope property. Syncs from scope → DOM using an effect, and from DOM → scope by listening to input or change events. Supported element types:
input[type=text] and other text inputs — listens to inputinput[type=checkbox] — syncs checked, listens to changeinput[type=radio] — checks when value matches, listens to changeselect — syncs value, listens to changetextarea — listens to input<input type="text" js-model="name" placeholder="Your name" />
<textarea js-model="bio"></textarea>
<input type="checkbox" js-model="agreed" />
<input type="radio" value="light" js-model="theme" />
<input type="radio" value="dark" js-model="theme" />
<select js-model="country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
name), not a full expression (not user.name). The directive reads the signal directly by key for two-way binding.
js-once="expression" (or use alongside js-text)
Evaluate the expression once at mount time, with no reactive tracking. The DOM is not updated if signals referenced by the expression change later. Useful for rendering initial values that should never change (timestamps, user IDs, etc.).
<!-- Timestamp set once on mount, never updates -->
<span js-once js-text="startedAt"></span>
<!-- Attribute set once -->
<input js-once js-bind:placeholder="defaultPlaceholder" />
js-ref="name"
Assign the DOM element to a signal in the scope. The value is the element itself. On cleanup (unmount), the signal is reset to null to prevent dangling references.
<input js-ref="taskInput" type="text" js-model="newTask" />
createApp({
newTask: '',
taskInput: null, // will be the <input> element after mount
focusInput() {
// this.taskInput is auto-unwrapped — it's the element directly
if (this.taskInput) this.taskInput.focus();
},
});
js-cloak (no value)
A marker attribute that is stripped during scouring. Combined with a CSS rule, it hides uninitialized content and reveals it once Signet mounts. No value or expression needed.
/* In your stylesheet or style block */
[js-cloak] { display: none; }
<div js-cloak js-scope="{ count: 0 }">
<!-- Hidden until mounted; no flash of js-text="count" -->
<span js-text="count"></span>
</div>
Register your own directives with .directive(name, handler). The handler receives a context object and can return an optional cleanup function.
| Property | Type | Description |
|---|---|---|
| el | Element | The bound DOM element |
| exp | string | The raw expression string |
| arg | string|null | Argument after : (e.g., "click" from js-on:click) |
| modifiers | string[] | Dot-separated modifiers (["prevent", "stop"]) |
| scope | object | The reactive scope object |
| effect | function | The effect() function from signals-core |
import { createApp, evaluate } from 'signet.js';
createApp({ tooltip: 'Hello!' })
// js-tooltip="tooltipText"
.directive('tooltip', ({ el, exp, scope, effect }) => {
// effect() re-runs whenever signals read inside change
const dispose = effect(() => {
el.title = evaluate(exp, scope);
});
// Return cleanup to dispose the effect on unmount
return dispose;
})
.mount('#app');
// js-color:background="expr" or js-color:text="expr"
.directive('color', ({ el, exp, arg, scope, effect }) => {
return effect(() => {
const val = evaluate(exp, scope);
if (arg === 'background') el.style.backgroundColor = val;
else if (arg === 'text') el.style.color = val;
});
})