Interactivity
How to add dynamic behavior with native HTML5 and Stimulus instead of component state.
HTML5 first
Before reaching for JavaScript, check if the browser already handles it. These native elements cover a surprising number of UI patterns:
| Pattern | HTML5 | Instead of |
|---|---|---|
| Modal / dialog | <dialog> |
Vue <Teleport> + state management |
| Popover / tooltip | [popover] attribute |
Headless UI / Floating UI |
| Accordion / disclosure | <details> / <summary> |
Disclosure components with state |
| Progress indicator | <progress> |
Custom progress bar components |
| Form validation | Built-in constraint API | Custom validation libraries |
These work without any JavaScript, are accessible by default, and are supported in all modern browsers.
<% # A modal using native <dialog> %>
<dialog id="confirm-dialog">
<%= kui(:card) do %>
<%= kui(:card, :header) do %>
<%= kui(:card, :title) { "Are you sure?" } %>
<% end %>
<%= kui(:card, :content) do %>
<p>This action cannot be undone.</p>
<% end %>
<%= kui(:card, :footer) do %>
<%= kui(:button, variant: :outline, data: { action: "click->dialog#close" }) { "Cancel" } %>
<%= kui(:button, variant: :solid, color: :error) { "Delete" } %>
<% end %>
<% end %>
</dialog>
Stimulus for everything else
When you need behavior that HTML can’t express — toggling visibility, clipboard operations, form enhancement, async loading — use Stimulus.
If you’re used to Vue’s reactivity or React’s useState, the mental shift
is: the server sends the right HTML, and Stimulus adds progressive
enhancements on top.
<% # Toggle visibility with Stimulus %>
<div data-controller="toggle">
<%= kui(:button, data: { action: "click->toggle#toggle" }) { "Show details" } %>
<div data-toggle-target="content" class="hidden">
<%= kui(:alert, color: :info) do %>
<%= kui(:alert, :description) { "Here are the details." } %>
<% end %>
</div>
</div>
Turbo for page updates
Rails uses Turbo for SPA-like page transitions and partial updates without writing JavaScript:
- Turbo Drive — automatic link interception, no full page reloads
- Turbo Frames — update a section of the page without touching the rest
- Turbo Streams — server-pushed DOM updates over WebSocket or HTTP
Kiso components work naturally with Turbo because they’re just HTML. Wrap a
component in a <turbo-frame> and it updates in place:
<turbo-frame id="notification-count">
<%= kui(:badge, color: :error, variant: :solid) { @count } %>
</turbo-frame>
Key difference from Vue/React
In Vue or React, the component owns its state and updates the DOM when state changes. In Kiso’s model:
- The server owns the state — your Rails models and controllers
- ERB renders the HTML — Kiso components produce the markup
- Turbo delivers updates — partial page updates without full reloads
- Stimulus adds behavior — small JS enhancements where HTML isn’t enough
This means less JavaScript, simpler debugging (view source works), and no hydration mismatches.