From installation to your first interactive component in five minutes.
Choose the method that fits your project.
Drop a single script tag into any HTML file. Add defer and init to enable auto-discovery of js-scope elements.
<script src="https://unpkg.com/signet.js" defer init></script>
npm install signet.js
Then import in your JS/module script:
import { createApp } from 'signet.js';
// CSP-safe, uses recursive descent parser
// Or, for a smaller bundle (requires unsafe-eval CSP):
import { createApp } from 'signet.js/unsafe';
Works in modern browsers with no bundler. Point the importmap at your installed copy or a CDN.
<script type="importmap">
{
"imports": {
"@preact/signals-core": "https://esm.sh/@preact/signals-core@1",
"signet.js": "./node_modules/signet.js/src/signet.js"
}
}
</script>
<script type="module">
import { createApp } from 'signet.js';
createApp({ count: 0 }).mount('#app');
</script>
js-scopeThe fastest way to add interactivity — zero JavaScript required beyond the script tag.
When loaded with defer init, Signet scans the document for all top-level js-scope elements and mounts each one automatically. Nested js-scope children are handled by their parent's walk.
<!DOCTYPE html>
<html>
<head>
<!-- Hide content until Signet mounts to prevent layout flash -->
<style>[js-cloak] { display: none; }</style>
<script src="https://unpkg.com/signet.js" defer init></script>
</head>
<body>
<div js-cloak js-scope="{ count: 0 }">
<p>Count: <strong js-text="count"></strong></p>
<button js-on:click="count++">+1</button>
<button js-on:click="count--">-1</button>
<button js-on:click="count = 0">Reset</button>
</div>
<!-- Multiple independent regions -->
<div js-cloak js-scope="{ open: false }">
<button js-on:click="open = !open">Toggle details</button>
<div js-show="open">Hidden content revealed!</div>
</div>
</body>
</html>
js-cloak with display:none to hide uninitialized content. Signet removes the js-cloak attribute on mount, making the element visible.
createApp() PatternUse createApp() when you need computed properties, methods, a global store, or custom directives.
<div id="app" js-cloak>
<h1 js-text="greeting"></h1>
<input js-model="name" placeholder="Your name" />
<p js-show="name.length > 0"
js-text="'Characters: ' + name.length"></p>
<ul>
<li js-for="item in items" js-text="item"></li>
</ul>
</div>
<script type="module">
import { createApp } from 'signet.js';
createApp({
// Plain values become signals automatically
name: '',
items: ['Apples', 'Bananas', 'Cherries'],
// Getters become computed signals
// `this.prop` auto-unwraps — no .value needed
get greeting() {
return this.name
? 'Hello, ' + this.name + '!'
: 'What is your name?';
},
// Methods can read AND write via this.prop
clearName() {
this.name = '';
},
})
.mount('#app');
</script>
createApp() returns a builder. Add shared state with .store(), register custom directives with .directive(), then call .mount().
import { createApp } from 'signet.js';
createApp({ count: 0 })
// Global state shared with all scopes
.store({ theme: 'dark', user: { name: 'Alice' } })
// Custom directive: js-highlight
.directive('highlight', ({ el, exp, scope, effect }) => {
return effect(() => {
const color = evaluate(exp, scope);
el.style.background = color || '';
});
})
.mount('#app');
Signet.js ships two imports. Both share the same directive engine — only the expression evaluator differs.
new Function()unsafe-eval in CSP// Default — CSP-safe
import { createApp } from 'signet.js';
// Smaller — unsafe-eval required
import { createApp } from 'signet.js/unsafe';
Before Signet mounts, directive expressions are visible as text. Use js-cloak to hide content until mounting is complete.
<style>
[js-cloak] { display: none; }
</style>
<!-- This div is invisible until Signet removes the js-cloak attribute -->
<div js-cloak js-scope="{ count: 0 }">
<span js-text="count"></span>
</div>