SensibleJS

Add reactivity to HTML with simple attributes. No build step. No virtual DOM. No dependencies. Just drop it in.

~10KB
Minified
0
Dependencies
18
Directives
1
File
Three steps. That's it.
No npm install. No webpack. No config files.
1

Include the script

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

Define your data

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

Use directives in your HTML

<input s-bind="name"> <p>s-bind="name"Hello1, {name}!</p> <button s-click="count++">Clicked {count} times</button>
s-bind
Bind any input to a variable. Change one, the other updates instantly.
Live s-bind
Hello, {demoName}
color: {demoColor} · size: {demoSize}px
View code
<!-- HTML --> <input type="text" s-bind="demoName"> <input type="color" s-bind="demoColor"> <input type="range" s-bind="demoSize" min="20" max="200"> <!-- Output: binds the same variable, with dynamic styling --> <span s-bind="demoName" s-css="color: {demoColor}; font-size: {demoSize + 'px'}"> Hello, {demoName} </span> <!-- Store --> <script> let store = { data: { demoName: { type: String, default: 'SensibleJS' }, demoColor: { type: String, default: '#6366f1' }, demoSize: { type: Number, default: 32 } } }; </script>
s-if
Show or hide elements based on any expression. No class toggling needed.
Live s-if
Welcome back! This element is conditionally visible.
These details only appear when the checkbox is checked. Toggle it to see the element appear and disappear from the DOM flow.
You can also use negation: this shows when greeting is hidden.
View code
<!-- Checkbox controls a Boolean variable --> <input type="checkbox" s-bind="showGreeting"> Show greeting <!-- Show when true --> <div s-if="showGreeting">Welcome back!</div> <!-- Show when false (negation) --> <div s-if="!showGreeting">Greeting is hidden</div> <!-- Works with any JS expression --> <button s-if="items.length > 0">Checkout</button> <p s-if="name != '' && name.length > 3">Hello!</p> <!-- Store --> <script> let store = { data: { showGreeting: { type: Boolean, default: true }, showDetails: { type: Boolean, default: false } } }; </script>
s-transition
Animate elements when they enter or leave. Add s-transition="name" to any element with s-if. SensibleJS applies CSS classes at each stage — you define the transitions in CSS.
Live s-transition
This element fades in and out.
This element slides in and out.
View code
<!-- Add s-transition to any s-if element --> <div s-if="show" s-transition="fade">Fades in/out</div> <div s-if="show" s-transition="slide">Slides in/out</div> <!-- Define the transition in CSS --> <style> /* Fade transition */ .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; } /* Slide transition */ .slide-enter { opacity: 0; transform: translateY(-10px); } .slide-enter-active, .slide-leave-active { transition: opacity 0.3s, transform 0.3s; } .slide-enter-to { opacity: 1; transform: translateY(0); } .slide-leave-to { opacity: 0; transform: translateY(-10px); } </style> <!-- Class lifecycle: Enter: {name}-enter → {name}-enter-active + {name}-enter-to → cleanup Leave: {name}-leave → {name}-leave-active + {name}-leave-to → display:none -->
s-for
Render lists from arrays. Add, remove, and reorder items reactively.
Live s-for
{tasks.length} task(s) ·
#{task.id} {task.text}
No tasks yet. Add one!
View code
<!-- HTML: iterate over an array --> <div s-for="task of tasks" s-key="task.id"> <span>#{task.id}</span> <span>{task.text}</span> <button onclick="removeTask(this)">remove</button> </div> <!-- Store --> <script> let store = { data: { tasks: { type: Array, default: [ { id: 1, text: 'First task' }, { id: 2, text: 'Second task' } ] }, newTask: { type: String, default: '', persist: false } } }; // Add items reactively function addTask() { tasks.push({ id: Date.now(), text: newTask }); newTask = ''; } // Remove by key from s-key-value attribute function removeTask(btn) { let key = btn.closest('[s-key-value]').getAttribute('s-key-value'); for (let i = 0; i < tasks.length; i++) { if (String(tasks[i].id) === key) { tasks.splice(i, 1); return; } } } </script>
s-css
Bind CSS properties directly to variables. Supports expressions and ternaries.
Live s-css
background: {cssColor} · radius: {cssRadius}px · opacity: {cssOpacity}%
View code
<!-- Bind CSS properties to variables --> <div s-css="background-color: {cssColor}; border-radius: {cssRadius + 'px'}; opacity: {cssOpacity / 100}"> </div> <!-- Ternary expressions work too --> <div s-css="background-color: {score >= 50 ? 'green' : 'red'}"> {score}% </div> <!-- Direct variable binding (no braces needed) --> <div s-css="background-color: bgColor; color: textColor"> Styled by variables </div>
Computed Values
Define variables that automatically recalculate when their dependencies change. No manual wiring needed.
Live computed
{compQty} × ${compPrice}
Subtotal: ${compSubtotal}
Tax ({compTaxRate}%): ${compTax}
Total: ${compTotal}
View code
<!-- Computed values update automatically --> <input type="number" s-bind="compPrice"> <input type="range" s-bind="compQty"> <!-- These update reactively --> <span s-bind="compSubtotal">{compSubtotal}</span> <span s-bind="compTotal">{compTotal}</span> <!-- Store: use computed instead of type/default --> <script> let store = { data: { compPrice: { type: Number, default: 25 }, compQty: { type: Number, default: 3 }, compTaxRate: { type: Number, default: 7 }, // Computed: expression string, re-evaluated on access compSubtotal: { computed: 'compPrice * compQty' }, compTax: { computed: 'Math.round(compPrice * compQty * compTaxRate) / 100' }, compTotal: { computed: 'compSubtotal + compTax' } } }; </script>
s-class, s-attr & s-debounce
Toggle CSS classes and HTML attributes based on expressions. Add s-debounce="ms" to text inputs to delay updates until the user stops typing — useful for validation, search, or API calls where you don't want to react on every keystroke.
Live s-class · s-attr · s-debounce
Both inputs use s-debounce="300" — the validation waits 300ms after you stop typing before updating. Try typing quickly vs. slowly to see the difference.
Form is valid Fill in both fields correctly
View code
<!-- s-debounce: delay binding updates (in milliseconds) --> <!-- Without it, the variable updates on every keystroke. --> <!-- With it, updates wait until the user pauses typing. --> <input s-bind="username" s-debounce="300"> <!-- s-class: toggle CSS classes based on expressions --> <div s-class="valid-border: formOk; invalid-border: !formOk"> Form status </div> <!-- s-attr: set/remove HTML attributes dynamically --> <!-- false/null/undefined removes the attribute --> <button s-attr="disabled: !formOk">Submit</button> <!-- Computed value: derived from other variables --> <script> let store = { data: { username: { type: String, default: '' }, email: { type: String, default: '' }, formOk: { computed: "username.length >= 3 && email.indexOf('@') >= 0" } } }; </script>
s-on
Bind any DOM event with modifiers. Supports .prevent, .stop, .enter, and .escape.
Live s-on
{onDemoLog}
View code
<!-- Bind keyboard events with modifiers --> <input s-bind="message" s-on="keydown.enter: addLog()"> <!-- Multiple events on one element --> <div s-on="mouseover: hovered = true; mouseout: hovered = false"> <!-- Prevent default (e.g. form submit) --> <form s-on="submit.prevent: handleSubmit()"> <!-- Modifiers: .prevent, .stop, .enter, .escape -->
s-ref
Name elements with s-ref and access them in expressions via $refs.name. Useful for focusing inputs, reading dimensions, or calling DOM methods.
Live s-ref
View code
<!-- Name an element with s-ref --> <input type="text" s-ref="myInput"> <!-- Access it via $refs in any expression --> <button s-click="$refs.myInput.focus()">Focus</button> <button s-click="$refs.myInput.value = ''">Clear</button> <!-- Works in any directive expression --> <p s-if="$refs.panel.scrollHeight > 200">Content is scrollable</p>
s-text & s-html
Set element content directly from an expression. s-text sets safe text content, s-html sets raw HTML. Simpler than s-bind for display-only elements.
Live s-text · s-html
View code
<!-- s-text: sets textContent (HTML tags are escaped) --> <p s-text="username"></p> <!-- s-html: sets innerHTML (renders HTML) --> <div s-html="richContent"></div> <!-- Expressions work too --> <span s-text="'Hello, ' + name"></span> <div s-html="'<strong>' + title + '</strong>'"></div> <!-- Use s-text for user input (XSS safe) Use s-html only for trusted content -->
s-bind & s-src
Bind image sources to variables. Use s-src inside loops for dynamic galleries.
Live s-bind · s-src
{photos.length} photo(s) ·
Avatar
{avatarUrl}
View code
<!-- s-bind on <img> sets the src attribute --> <input type="text" s-bind="avatarUrl"> <img s-bind="avatarUrl"> <!-- s-src for dynamic sources inside loops --> <div s-for="photo of photos" s-key="photo.id"> <img s-src="{photo.url}"> <button onclick="removePhoto(this)">×</button> </div> <!-- Store --> <script> let store = { data: { avatarUrl: { type: String, default: 'https://picsum.photos/id/64/200' }, photos: { type: Array, default: [ { id: 1, url: 'https://picsum.photos/id/10/200' }, { id: 2, url: 'https://picsum.photos/id/20/200' } ], persist: false } } }; // Remove by key — same pattern as any s-for list function removePhoto(btn) { let key = btn.closest('[s-key-value]').getAttribute('s-key-value'); for (let i = 0; i < photos.length; i++) { if (String(photos[i].id) === key) { photos.splice(i, 1); return; } } } </script>
s-click
Execute expressions on click. Simple counters, toggles, or function calls.
Live s-click
{clickCount} clicks
You're on fire! The color changed because clickCount > 10.
View code
<!-- Inline expressions — no functions needed --> <button s-click="clickCount++">Click me</button> <button s-click="clickCount = 0">Reset</button> <!-- Combine with s-bind and s-css for reactive feedback --> <span s-bind="clickCount" s-css="color: {clickCount > 10 ? 'green' : 'white'}"> {clickCount} clicks </span> <!-- Push to arrays, toggle booleans, call functions --> <button s-click="items.push({id: Date.now(), text: 'New'})">Add Item</button> <button s-click="menuOpen = !menuOpen">Toggle Menu</button> <!-- Store --> <script> let store = { data: { clickCount: { type: Number, default: 0 } } }; </script>
localStorage Persistence
Variables survive page refreshes automatically. No extra code needed.
Live persist: true
{persistDemo} Type above, then reload the page. Your text will still be here.
View code
<!-- Just bind an input — persistence is automatic --> <input type="text" s-bind="persistDemo"> <!-- Store: persist is true by default --> <script> let store = { persist: true, // Save all variables to localStorage localPrefix: '__', // Key prefix to avoid collisions data: { persistDemo: { type: String, default: '' }, // Override per variable: userPrefs: { type: Object, default: {}, persist: true }, // always saved tempInput: { type: String, default: '', persist: false } // never saved } }; </script>
Watchers
Add a watch function to any store variable. It receives (newValue, oldValue) on every change — useful for side effects, validation, or derived updates.
Live watch
{watchLog}
View code
<!-- Watchers are defined in the store --> <script> let store = { data: { username: { type: String, default: '', watch: function(newVal, oldVal) { console.log('Changed from', oldVal, 'to', newVal); } } } }; </script> <!-- watch receives (newValue, oldValue) — useful for comparisons, validation, or triggering side effects -->
onInit Lifecycle Hook
Run setup code after SensibleJS finishes initializing. The onInit function receives the data object, so you can set initial values, fetch data, or trigger side effects.
Live onInit
{initMessage}

This message was set by onInit when the page loaded.

View code
<script> let store = { data: { message: { type: String, default: '' } }, onInit: function(data) { // Runs after all data is bound and DOM is ready data.message = 'Initialized at ' + new Date().toLocaleTimeString(); // Common uses: // - Set values based on URL params // - Fetch initial data from an API // - Run one-time setup logic } }; </script>
s-cloak — Hide Until Ready
Prevents the flash of raw template expressions (like {name}) before SensibleJS initializes. Elements with s-cloak stay hidden until all data is bound, then the attribute is automatically removed.
Live s-cloak

The greeting below uses s-cloak — it was hidden until SensibleJS finished loading, so you never saw the raw {cardName} template.

Welcome, {cardName}! Enter a name in the mini-app below to see s-cloak in action.
View code
<!-- Without s-cloak: user briefly sees "{name}" --> <p s-bind="name">Hello, {name}!</p> <!-- With s-cloak: element stays hidden until data is ready --> <p s-cloak s-bind="name">Hello, {name}!</p> <!-- No CSS needed — SensibleJS injects the rule automatically: [s-cloak] { display: none !important } The attribute is removed after initialization. -->
s-unclick
Execute an expression when a click occurs outside the element. Perfect for closing dropdowns, modals, and popovers.
Live s-unclick
Edit profile
Settings
Sign out
Click the button, then click anywhere outside the dropdown to close it.
View code
<!-- Wrap button + dropdown so clicks inside don't close it --> <div s-unclick="menuOpen = false"> <button s-click="menuOpen = !menuOpen">Menu</button> <!-- Dropdown closes when you click outside the wrapper --> <div s-if="menuOpen"> <div>Edit</div> <div>Settings</div> <div>Sign out</div> </div> </div>
s-data
Define variables directly in HTML without a store. Useful for quick prototypes or self-contained widgets.
Live s-data
Counter: {inlineCount}

No <script> store needed — variables are defined right in the HTML with s-data.

View code
<!-- Define variables directly in HTML --> <div s-data="{count: 0, message: 'Hello!'}"> <span s-text="message"></span> <span s-bind="count">{count}</span> <button s-click="count++">Increment</button> </div> <!-- No store needed — great for quick prototypes -->
A Complete Mini-App
All directives working together in a profile card builder.
Live all directives
{skill.name}
Avatar
{cardEmail}
{skill.name}
{cardSkillCount} skill(s)
Fill in the fields to see your card.
View code
<!-- This mini-app uses every SensibleJS directive --> <!-- s-bind, s-debounce, s-class: validated name input --> <input s-bind="cardName" s-debounce="200" s-class="valid: cardName.length >= 2" s-ref="nameInput"> <!-- s-blur: avatar updates on blur, not keyup --> <input s-bind="cardAvatar" s-blur> <!-- s-if + s-transition: animated email reveal --> <div s-if="showEmail" s-transition="slide">...</div> <!-- s-on: add skill on Enter key --> <input s-on="keydown.enter: addSkill()"> <!-- s-for + s-key: render skill tags --> <span s-for="skill of skills" s-key="skill.id">{skill.name}</span> <!-- s-click: add skill --> <button s-click="addSkill()">Add</button> <!-- s-ref: focus name input from button --> <button s-click="$refs.nameInput.focus()">Focus</button> <!-- s-attr: disable button when form is default --> <button s-attr="disabled: !changed">Reset</button> <!-- Card preview: s-css, s-text, s-html, s-src --> <div s-css="border-color: {accent}"> <img s-src="{avatar}"> <div s-text="name"></div> <!-- safe text --> <div s-html="bio"></div> <!-- renders HTML --> <span s-bind="skillCount">{skillCount} skill(s)</span> <!-- computed --> </div>