A reactive UI system built on top of Stimulus.js that enables declarative, state-driven interfaces with minimal JavaScript. This project demonstrates how to create dynamic, interactive web applications using reactive patterns within the Stimulus framework.
Tip
<div data-controller="live">
<!-- State -->
<input type="hidden" data-live-state="count" value="0" />
<!-- Computed properties -->
<script type="text/template" data-live-computed="isPositive">
state.count > 0
</script>
<script type="text/template" data-live-computed="statusText">
state.count < 0 ? 'negative' : 'positive'
</script>
<!-- Demo -->
<h2 live:text="state.count">0</h2>
<p live:class="{ 'bg-green-100': computed.isPositive, 'bg-red-100': !computed.isPositive }">
Counter is <span live:text="computed.statusText">positive</span>
</p>
<button data-action="click->live#update" data-live-update-param="state.count++">Increment</button>
<button data-action="click->live#update" data-live-update-param="state.count--">Decrement</button>
</div>The demo is entirely self-contained in a single index.html file that showcases 13 different interactive examples. Everything needed to run the demo is included:
- Tailwind CSS - Loaded via CDN for styling
- Stimulus.js - Imported via ES modules from unpkg
- LiveController - Custom Stimulus controller that implements the reactive system
- Interactive Examples - 11 demonstrations of reactive UI patterns
- Declarative Reactive Bindings - Use
live:text,live:class,live:show, etc. to bind state to DOM elements - Computed States - Define derived state using template scripts
- Two-way Data Binding - Automatic synchronization between form inputs and state
- Conditional Rendering - Show/hide elements based on state
- Dynamic Styling - Apply CSS classes and styles reactively
- State Management - Clean, predictable state updates with automatic UI synchronization
Define reactive state properties that can be bound to form inputs or calculated dynamically.
Form Input Binding - Use data-live-state="propertyName" on inputs:
<input type="text" data-live-state="username" value="john" />
<input type="checkbox" data-live-state="isActive" />
<select data-live-state="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>Computed States - Use <script type="text/template" data-live-computed="propertyName"> with JavaScript expressions:
<script type="text/template" data-live-computed="fullName">
state.firstName + ' ' + state.lastName
</script>
<script type="text/template" data-live-computed="isValid">
state.username.length > 3 && state.email.includes('@')
</script>HTML Template Literals - When a computed property starts with <, it's automatically treated as an HTML template literal:
<!-- Simple HTML templates with interpolation -->
<script type="text/template" data-live-computed="userCard">
<div class="user-card">
<h3>${state.userName}</h3>
<p>Status: ${computed.userStatus}</p>
</div>
</script>
<!-- Complex templates with conditional content -->
<script type="text/template" data-live-computed="notificationHtml">
<div class="notification ${computed.notificationType}">
<strong>${computed.title}</strong>
<p>${state.message}</p>
${computed.showTimestamp ? `<small>${computed.timestamp}</small>` : ''}
</div>
</script>Important: Within computed property definitions, use
state.to reference base state properties andcomputed.to reference other computed properties. In your HTML templates, usecomputed.to reference computed properties andstate.for base state properties.
<!-- In computed definitions -->
<script type="text/template" data-live-computed="greeting">
'Hello, ' + state.firstName + ' ' + state.lastName
</script>
<script type="text/template" data-live-computed="formalGreeting">
computed.greeting + '! Welcome to our application.'
</script>
<!-- In HTML templates -->
<h1 live:text="computed.greeting">Hello</h1>
<p live:text="computed.formalGreeting">Welcome</p>Bind state to DOM elements using special live:* attributes that automatically update when state changes.
| Attribute | Description | Example |
|---|---|---|
live:text |
Sets element text content | live:text="state.count" |
live:html |
Sets element innerHTML | live:html="state.content" |
live:class |
Conditionally applies classes | live:class="{ 'active': state.isActive }" |
live:style |
Sets CSS styles | live:style="{ color: state.textColor }" |
live:show |
Shows/hides element | live:show="state.isVisible" |
live:disabled |
Enables/disables element | live:disabled="!state.isValid" |
live:attributes |
Sets multiple HTML attributes | live:attributes="{ src: state.url, alt: state.title }" |
Examples:
<!-- Text and HTML content -->
<h1 live:text="state.title">Default Title</h1>
<div live:html="computed.htmlContent"></div>
<!-- Conditional styling -->
<button
live:class="{
'btn-primary': computed.isActive,
'btn-secondary': !computed.isActive,
'disabled': computed.isLoading
}"
>
Submit
</button>
<!-- Dynamic styles -->
<div
live:style="{
width: computed.progress + '%',
backgroundColor: state.color
}"
></div>
<!-- Show/hide and enable/disable -->
<div live:show="computed.showDetails">Details content</div>
<button live:disabled="!computed.isValid">Save</button>
<!-- Multiple attributes at once -->
<img
live:attributes="{
src: computed.imageUrl,
alt: computed.imageDescription,
title: computed.imageTitle
}"
/>
<input
live:attributes="{
placeholder: computed.placeholderText,
'data-tooltip': computed.helpText,
'aria-label': computed.accessibilityLabel
}"
/>State Updates - Trigger state changes using the live#update action:
<button data-action="click->live#update" data-live-update-param="state.count++">Increment</button>
<button data-action="click->live#update" data-live-update-param="state.isVisible = !state.isVisible">Toggle</button>This project uses modern JavaScript features including:
- ES6 Modules
- Proxy objects
- MutationObserver
- Template literals
Supported in all modern browsers (Chrome 61+, Firefox 60+, Safari 12+, Edge 79+).
When implementing reactive UI systems that interpolate user input, it's important to consider security implications. This demo evaluates dynamic code using new Function(), which is safer and more performant than eval() but still carries risks with untrusted input.
For production applications planning to interpolate user input, it is recommended to restrict JavaScript execution to a safe subset using libraries like jse-eval, which provides secure expression evaluation without full JavaScript access. Always validate and sanitize user input before incorporating it into reactive expressions to prevent cross-site scripting (XSS) and code injection vulnerabilities.