Getting Started

Set up Signet.js

From installation to your first interactive component in five minutes.

Installation

Choose the method that fits your project.

Option A — CDN (no tooling)

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>

Option B — npm

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';

Option C — ESM with importmap

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>

Auto-Init with js-scope

The 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>
Tip: Use js-cloak with display:none to hide uninitialized content. Signet removes the js-cloak attribute on mount, making the element visible.

The createApp() Pattern

Use 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>

Chaining: store, directive, mount

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');

Two Entry Points

Signet.js ships two imports. Both share the same directive engine — only the expression evaluator differs.

signet.js Default
  • ✅ CSP-safe (no eval)
  • ✅ Recursive descent parser
  • ✅ Works in hardened environments
  • ⚠️ Slightly larger bundle
signet.js/unsafe Smaller
  • ✅ Smaller bundle (parser excluded)
  • ✅ Full JS expression support
  • ❌ Uses new Function()
  • ❌ Requires unsafe-eval in CSP
// Default — CSP-safe
import { createApp } from 'signet.js';

// Smaller — unsafe-eval required
import { createApp } from 'signet.js/unsafe';

Preventing Flash of Unstyled Content

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>

Next Steps