npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sensibljs

v1.7.0

Published

Reactive UI in ~5KB gzipped (18KB minified). No build step. No virtual DOM. No dependencies. Just HTML attributes.

Readme

SensibleJS

Reactive UI in ~5KB gzipped (18KB minified). No build step. No virtual DOM. No dependencies. Just HTML attributes.

<script src="sensibljs.min.js" defer></script>

<input s-bind="name" placeholder="Your name">
<p s-bind="name">Hello, {name}!</p>

That's a working two-way binding. Type in the input, the paragraph updates.

Install

CDN / direct download (recommended for what this tool is designed for):

<script src="sensibljs.min.js" defer></script>

npm:

npm i sensibljs

Quick Start

Define a store with your variables, then use directives in your HTML:

<script src="sensibljs.min.js" defer></script>

<input s-bind="name" placeholder="Name">
<p s-bind="name" s-if="name.length > 0">Hello, {name}!</p>
<button s-click="count++">Clicked <span s-bind="count">{count}</span> times</button>

<script>
let store = {
    persist: true,
    data: {
        name:  { type: String, default: '' },
        count: { type: Number, default: 0 }
    }
};
</script>

That's it. No initialization call needed — SensibleJS detects the store and starts automatically.

Store Configuration

The store object controls persistence and declares your reactive variables:

let store = {
    persist: true,       // Save to localStorage (default: true)
    localPrefix: '__',   // Prefix for stored keys (default: '__')
    data: {
        username: {
            type: String,
            default: 'guest',
            persist: true,           // Per-variable persistence override
            watch: function(newVal, oldVal) {   // Runs when the value changes
                console.log('Changed from', oldVal, 'to', newVal);
            }
        },
        items: {
            type: Array,
            default: [{ id: 1, text: 'First item' }],
            persist: false
        },
        settings: {
            type: Object,
            default: { theme: 'dark', lang: 'en' }
        },
        visible: {
            type: Boolean,
            default: true
        },
        count: {
            type: Number,
            default: 0
        }
    }
};

Variable Types

| Type | Description | | --- | --- | | String | Text values. Default input type. | | Number | Numeric values. Works with number, range inputs. | | Boolean | True/false. Works with checkbox inputs. | | Array | Lists. Used with s-for. Mutations (push, pop, splice, etc.) are observed. | | Object | Key-value pairs. Property changes are observed. |

Variable Options

| Option | Description | | --- | --- | | type | JavaScript type constructor (String, Number, Array, etc.) | | default | Initial value | | persist | true/false — override the store-level persist setting for this variable | | computed | Expression string — makes this a read-only computed variable (no type or default needed) | | watch | Function called with (newValue, oldValue) on every change |

If no store is defined, SensibleJS creates one automatically and detects variables from s-bind attributes in the DOM.

Directives

s-bind — Two-Way Data Binding

Connects an element to a variable. Works with inputs, textareas, selects, and display elements.

<!-- Text input: updates on keyup -->
<input type="text" s-bind="name">

<!-- Update on blur instead of keyup -->
<input type="text" s-bind="name" s-blur>

<!-- Display the value (with template expressions) -->
<p s-bind="name">Hello, {name}!</p>

<!-- Checkbox: binds to a Boolean -->
<input type="checkbox" s-bind="isActive">

<!-- Radio buttons -->
<input type="radio" s-bind="color" value="red" name="color"> Red
<input type="radio" s-bind="color" value="blue" name="color"> Blue

<!-- Select -->
<select s-bind="option">
    <option value="a">Option A</option>
    <option value="b">Option B</option>
</select>

<!-- Color, date, number, range -->
<input type="color" s-bind="themeColor">
<input type="date" s-bind="startDate">
<input type="number" s-bind="quantity">
<input type="range" s-bind="volume" min="0" max="100">

<!-- Image src binding -->
<img s-bind="avatarUrl">

s-if — Conditional Rendering

Show or hide elements based on an expression. The element's original display style (flex, grid, inline, etc.) is preserved.

<div s-if="isLoggedIn">Welcome back!</div>
<div s-if="!isLoggedIn">Please log in.</div>

<!-- Works with any expression -->
<button s-if="items.length > 0">Checkout</button>
<p s-if="name != ''">Hello, {name}</p>

s-transition — Animated Show/Hide

Add s-transition="name" to any s-if element to animate enter/leave transitions with CSS classes instead of hard display toggling.

<div s-if="show" s-transition="fade">Fades in/out</div>

SensibleJS applies classes at each stage:

  • Enter: {name}-enter{name}-enter-active + {name}-enter-to → cleanup
  • Leave: {name}-leave{name}-leave-active + {name}-leave-todisplay: none

Define your transitions in CSS:

.fade-enter { opacity: 0; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-to { opacity: 1; }
.fade-leave-to { opacity: 0; }

If s-transition has no value (e.g., <div s-transition>), the prefix defaults to s-.

s-for — List Rendering

Iterate over arrays. Uses keyed reconciliation — existing DOM elements are reused, not rebuilt.

<ul>
    <li s-for="item of items" s-key="item.id">
        {item.name} - ${item.price}
    </li>
</ul>

<!-- Access the index -->
<div s-for="user of users" s-key="user.email">
    {index + 1}. {user.name} ({user.email})
</div>

<!-- Images inside loops -->
<div s-for="photo of photos">
    <img s-src="{photo.url}">
    <span>{photo.caption}</span>
</div>

The s-key attribute tells SensibleJS how to identify each item (e.g., s-key="item.id"). When items change, only affected elements are updated. Without s-key, the index is used.

s-css — Dynamic Styling

Bind CSS properties to variables or expressions.

<!-- Direct variable binding -->
<div s-css="background-color: bgColor; color: textColor">
    Styled content
</div>

<!-- Ternary expressions -->
<div s-css="background-color: {score >= 50 ? 'green' : 'red'}">
    {score}%
</div>

<!-- Template expressions -->
<div s-css="width: {percentage + '%'}; opacity: {opacity / 100}">
    Progress bar
</div>

s-class — Conditional CSS Classes

Toggle CSS classes based on expressions. Uses semicolon-separated class: expression pairs.

<!-- Toggle a single class -->
<input s-class="valid-border: email.length > 5; invalid-border: email.length <= 5">

<!-- Multiple classes on one element -->
<div s-class="active: isActive; highlighted: score > 90; dimmed: !isVisible">
    Content
</div>

s-attr — Dynamic HTML Attributes

Set or remove HTML attributes based on expressions. If the expression evaluates to false, null, or undefined, the attribute is removed. true sets an empty attribute (e.g., disabled). Any other value sets the attribute to that value.

<!-- Disable a button conditionally -->
<button s-attr="disabled: !formValid">Submit</button>

<!-- Dynamic href -->
<a s-attr="href: linkUrl; target: openInNew ? '_blank' : '_self'">Link</a>

<!-- Toggle aria attributes -->
<div s-attr="aria-hidden: !isVisible; aria-expanded: isOpen">Panel</div>

s-on — General Event Binding

Bind any DOM event with optional modifiers. Format: event.modifier: expression. Multiple bindings separated by semicolons.

Modifiers: | Modifier | Effect | | --- | --- | | .prevent | Calls event.preventDefault() | | .stop | Calls event.stopPropagation() | | .enter | Only fires on Enter key | | .escape | Only fires on Escape key |

<!-- Keyboard events -->
<input s-on="keydown.enter: submitForm()">
<div s-on="keydown.escape: closeModal()">

<!-- Prevent default form submission -->
<form s-on="submit.prevent: handleSubmit()">

<!-- Multiple events -->
<div s-on="mouseenter: hover = true; mouseleave: hover = false">
    Hover me
</div>

s-click — Click Handler

Execute an expression when the element is clicked.

<button s-click="count++">Increment</button>
<button s-click="count = 0">Reset</button>
<button s-click="items.push({id: Date.now(), text: 'New'})">Add Item</button>

s-unclick — Click Away Handler

Execute an expression when a click occurs outside the element.

<div s-unclick="menuOpen = false">
    <!-- Dropdown menu that closes when you click elsewhere -->
</div>

s-text — Safe Text Content

Set an element's textContent from an expression. HTML is escaped — safe for user input.

<p s-text="username"></p>
<span s-text="'Hello, ' + name"></span>

s-html — Raw HTML Content

Set an element's innerHTML from an expression. Only use with trusted content.

<div s-html="richContent"></div>
<div s-html="'<strong>' + title + '</strong>'"></div>

s-ref — Element References

Name elements and access them in expressions via $refs.

<input type="text" s-ref="myInput">
<button s-click="$refs.myInput.focus()">Focus</button>
<button s-click="$refs.myInput.value = ''">Clear</button>

s-cloak — Hide Until Ready

Prevents the flash of raw template expressions (e.g., {name}) before SensibleJS initializes. The element stays hidden until all data is bound, then the attribute is automatically removed.

<p s-cloak s-bind="name">Hello, {name}!</p>

No CSS needed — SensibleJS injects [s-cloak] { display: none !important } automatically.

s-data — Inline Variable Definition

Define variables directly in HTML without a store.

<div s-data="{isOpen: false, message: 'Hello'}">
    <button s-click="isOpen = !isOpen">Toggle</button>
    <div s-if="isOpen" s-bind="message">{message}</div>
</div>

s-debounce — Debounced Input

Delay reactive updates on text inputs until the user stops typing. Useful for search fields, API calls, or expensive computations. Value is in milliseconds.

<!-- Wait 300ms after the user stops typing before updating -->
<input s-bind="search" s-debounce="300">

<!-- Works with text, email, and textarea -->
<textarea s-bind="notes" s-debounce="500"></textarea>

Without s-debounce, the bound variable updates on every keystroke. With it, the update is delayed until the specified pause — reducing unnecessary re-renders and making downstream effects (like filtering a list or calling a function) more efficient.

s-src — Dynamic Image Source

Set an image's src attribute from a variable. Particularly useful inside s-for loops.

<img s-src="{user.avatar}">

Computed Values

Define variables whose values are derived from other variables. They update automatically whenever their dependencies change.

let store = {
    data: {
        price:    { type: Number, default: 10 },
        quantity: { type: Number, default: 1 },
        subtotal: { computed: 'price * quantity' },
        tax:      { computed: 'subtotal * 0.07' },
        total:    { computed: 'subtotal + tax' }
    }
};
<p s-bind="total">Total: ${total.toFixed(2)}</p>

Computed values are read-only getters — you can't assign to them directly. They recalculate whenever any variable referenced in their expression changes.

Watchers

Add a watch function to any variable to react to changes with both new and old values:

let store = {
    data: {
        search: {
            type: String,
            default: '',
            watch: function(newVal, oldVal) {
                console.log('Changed from', oldVal, 'to', newVal);
            }
        }
    }
};

The watch function receives (newValue, oldValue) as arguments, making it easy to compare values or trigger side effects based on what changed.

Lifecycle Hook: onInit

Run setup code after SensibleJS finishes initializing. The onInit function receives the reactive data object:

let store = {
    data: {
        message: { type: String, default: '' }
    },
    onInit: function(data) {
        data.message = 'Ready at ' + new Date().toLocaleTimeString();
        // Fetch data, read URL params, run setup logic, etc.
    }
};

Contenteditable Support

Elements with contenteditable="true" can be bound with s-bind for inline rich text editing:

<div contenteditable="true" s-bind="notes"></div>

The bound variable syncs with the element's innerText on every input event.

Template Expressions

Use {expression} syntax inside element content to embed dynamic values:

<p s-bind="name">Hello, {name}!</p>
<span s-bind="count">{count} item(s)</span>
<div s-bind="price">Total: ${price * 1.07}</div>

Expressions support standard JavaScript: ternaries, arithmetic, string concatenation, property access, and function calls like parseInt(), Math.max(), etc.

Security

Expressions are evaluated in a sandboxed scope. Dangerous globals (document, window, fetch, XMLHttpRequest, Function, setTimeout, setInterval) are blocked inside all directive expressions. Safe globals like Math, parseInt, JSON, and console remain accessible.

This means even if untrusted content is rendered into the DOM, expressions cannot:

  • Access or modify the document
  • Make network requests
  • Create new functions or use eval
  • Set timers

Persistence

When persist: true (the default), variable values are saved to localStorage and restored on page load. This means user input, preferences, and state survive page refreshes with zero additional code.

Control persistence per-variable:

data: {
    userPrefs: { type: Object, default: {}, persist: true },   // Saved
    tempInput: { type: String, default: '', persist: false }    // Not saved
}

Stored keys are prefixed with localPrefix (default '__') to avoid collisions.

Supported Input Types

| Input Type | Binding Behavior | | --- | --- | | text, email, textarea | Updates on keyup (or blur with s-blur) | | checkbox | Two-way boolean binding | | radio | Binds to the selected value | | select | Binds to the selected option value | | number, range | Updates on input event | | color | Updates on input event | | date, datetime-local | Updates on input event | | img | Sets the src attribute |

Browser Support

SensibleJS uses Object.defineProperty, Promise, Map, Set, and arrow functions. It works in all modern browsers (Chrome, Firefox, Safari, Edge). No IE11 support.

Contributing

Pull requests are welcome. See CONTRIBUTING.md for details.

License

MIT License - Copyright (c) 2026 Ricardo Aponte Yunqué

Acknowledgments

  • Blaize Stewart for the Observables logic
  • @stimulus for the domReady function