A tiny, Alpine-like reactive runtime that uses vanilla JS signals and squeaks fast.
hamsterio takes Alpine's delightful HTML-first syntax and marries it with Solid's signal-based reactivity. The result? A tiny powerhouse that delivers updates so fast they'll make your hamster wheel spin.
"Why hamsters? Because they're small, fast, and surprisingly powerful. Also, they fit in your pocket." πΉ
β
Tiny: Small enough to fit in a hamster's cheek pouch. It's under 3KB gzipped (~7KB minified).
β
Fast: Signal-based reactivity means surgical DOM updates, not sledgehammer re-renders.
β
Familiar: If you know Alpine.js, you already know hamsterio.
β
No Build Step: Drop it in via CDN and start coding. Your hamster doesn't have time for webpack configs.
β¬οΈ Migrating from hamsterx? This package was renamed from
hamsterxtohamsterioin v0.5.0. Update your directives fromx-*toh-*and you're good to go!
<script defer src="https://cdn.jsdelivr.net/npm/hamsterio@latest/dist/hamsterio.min.js"></script>The defer attribute is optional but recommended for performance - it lets your HTML load first before the hamster starts running.
npm install hamsterio// Import everything
import hamsterio from 'hamsterio';
// Or import just what you need
import { init, cleanup, createSignal, createEffect } from 'hamsterio';If you need manual control over initialisation, set this before loading hamsterio:
<!-- For CDN -->
<script>window.hamsterioAutoInit = false;</script>
<script defer src="https://cdn.jsdelivr.net/npm/hamsterio@latest/dist/hamsterio.min.js"></script>
<script defer>
// Now you control when to init
hamsterio.init();
console.log('πΉ hamsterio initialised!');
</script>Note: When using npm/modules, auto-init only works in browser environments. If you're using a bundler, you'll typically want to call init() manually anyway:
import hamsterio from 'hamsterio';
// Call init when your app is ready
hamsterio.init();When auto-init runs (CDN or browser usage), hamsterio dispatches a hamsterio:ready event on the document. This is useful if you need to run code after the library has initialised:
<script>
document.addEventListener('hamsterio:ready', () => {
console.log('πΉ hamster is ready to run!');
// Your initialisation logic here
});
</script>
<script defer src="https://cdn.jsdelivr.net/npm/hamsterio@latest/dist/hamsterio.min.js"></script>Note that init() is synchronous, so you don't need to wait for this event when calling init() manually.
<div h-data="{ count: 0 }">
<button h-on:click="count++">πΉ Feed the hamster</button>
<p h-text="count"></p>
<p h-show="count > 5">The hamster is getting chubby!</p>
</div>That's it. No compilation, no virtual DOM, no existential crisis about framework choices.
- Why hamsterio?
- Installation
- Quick Start
- Directives
- Transitions
- Real-World Examples
- Working with Signals
- Dynamic Content & Cleanup
- Programmatic Access
- Browser Support
- Size Comparison
- Caveats
- Contributing
- Roadmap
- Philosophy
- License
- Credits
Defines reactive data for a component. Think of it as the hamster's data pellets.
<div h-data="{ name: 'Whiskers', age: 2 }">
<!-- Your component here -->
</div>You can define methods that access your reactive data using this:
<div
h-data="{
count: 0,
increment() {
this.count++
},
reset() {
this.count = 0
}
}"
>
<button h-on:click="increment()">Add seed to pouches</button>
<button h-on:click="reset()">Empty the pouches</button>
<span h-text="count"></span>
</div>Methods have full access to all reactive data through this and can be called from any directive.
Reactively updates text content. Like a hamster's name tag that magically changes.
<span h-text="name"></span>Reactively updates inner HTML. For when your hamster needs to render rich content (use responsibly - sanitise user input!).
<div h-html="`<strong>${name} is hungry!</strong>`"></div>h-html with unsanitised user input. Your hamster doesn't want XSS vulnerabilities in its cage!
Toggles visibility based on a condition. Your hamster appears and disappears (it's not magic, just CSS).
<div h-show="isVisible">πΉ Peek-a-boo!</div>Reactively binds attributes. Your hamster's outfit changes with its mood.
<!-- Boolean attributes (hamsters can be disabled too) -->
<button h-bind:disabled="isLoading">Submit</button>
<input h-bind:readonly="!isEditing">
<!-- Dynamic attributes (hamster images are important) -->
<img h-bind:src="hamsterPhotoUrl" h-bind:alt="hamsterName">
<a h-bind:href="hamsterBlogUrl">Read more about hamsters</a>
<!-- Conditional classes (object syntax - the hamster's favorite) -->
<div h-bind:class="{ 'active': isActive, 'sleepy': !isAwake }">
Hamster status indicator
</div>
<!-- Dynamic styles (because hamsters appreciate good design) -->
<div h-bind:style="{ color: furColor, fontSize: size + 'px' }">
Color-coordinated hamster
</div>Class binding supports object syntax for conditional classes. Your original HTML classes are preserved (hamsters don't forget their roots):
<div class="hamster-card cozy" h-bind:class="{ 'running': isActive, 'napping': isLoading }">
Base classes stay, dynamic classes toggle like a hamster wheel
</div>Listens to events. Your hamster responds to pokes (gentle ones, we hope). With async support for hamsters who need to wait for things!
<button h-on:click="count++">Click me</button>
<input h-on:input="search = $event.target.value">
<!-- Async event handlers - because sometimes hamsters need to fetch snacks -->
<button h-on:click="await saveData(); showSuccess = true">
Save to hamster database
</button>
<form
h-on:submit="
$event.preventDefault();
const result = await fetch('/api/hamster-signup', {
method: 'POST',
body: JSON.stringify($data)
});
registered = await result.json();
"
>
<input h-bind:value="email" h-on:input="email = $event.target.value">
<button type="submit">Join the hamster club</button>
</form>Special variables:
$event- The native event object$el- The element itself$data- All your reactive data
Pro tip: Event handlers fully support await for async operations. Your hamster can now fetch data, call APIs, and wait for promises without breaking a sweat (or whisker).
Loops through arrays. Like multiple hamsters running on multiple wheels.
<!-- Simple syntax -->
<template h-for="item in items">
<li h-text="item"></li>
</template>
<!-- With index -->
<template h-for="(item, index) in items">
<li h-text="`${index}: ${item}`"></li>
</template>Runs initialisation code when your component first loads. Perfect for fetching data, setting up timers, or waking your hamster up in the morning. Fully supports await for async operations!
<!-- Simple initialisation -->
<div h-data="{ greeting: '' }" h-init="greeting = 'Hello from hamster HQ! πΉ'">
<p h-text="greeting"></p>
</div>
<!-- Fetch data on mount - hamsters love fresh data -->
<div
h-data="{ hamsters: [], loading: true }"
h-init="
hamsters = await (await fetch('/api/hamsters')).json();
loading = false;
"
>
<div h-show="loading">Loading hamster profiles...</div>
<ul h-show="!loading">
<template h-for="hamster in hamsters">
<li h-text="hamster.name"></li>
</template>
</ul>
</div>
<!-- Multiple async operations - because hamsters multitask -->
<div
h-data="{ user: null, settings: null, ready: false }"
h-init="
user = await (await fetch('/api/user')).json();
settings = await (await fetch('/api/settings')).json();
ready = true;
console.log('πΉ Hamster profile loaded!');
"
>
<div h-show="ready">
<h1 h-text="user.name"></h1>
<p h-text="`Favorite food: ${settings.favoriteSnack}`"></p>
</div>
</div>Key points:
- Runs once when the element is initialised (not reactive)
- Runs after all other directives are set up (so your bindings are ready)
- Fully supports
awaitfor fetching data or other async operations - Access to
$eland all reactive data via$data
Pro tip: Use h-init for data fetching, third-party library initialisation, or any setup logic your hamster needs before getting to work!
Make your hamster's entrances and exits graceful! hamsterio supports smooth transitions using h-transition-enter and h-transition-leave with h-show.
When you toggle visibility with h-show, hamsterio can apply CSS classes for smooth animations:
<style>
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.fade-in { animation: fadeIn 300ms ease-out; }
.fade-out { animation: fadeOut 300ms ease-in; }
</style>
<div h-data="{ visible: false }">
<button h-on:click="visible = !visible">
Toggle Hamster Visibility πΉ
</button>
<div
h-show="visible"
h-transition-enter="fade-in"
h-transition-leave="fade-out"
>
πΉ The hamster appears gracefully and exits with dignity!
</div>
</div>You can also use CSS transitions instead of animations:
<style>
.opacity-enter {
opacity: 1;
transition: opacity 300ms;
}
.opacity-leave {
opacity: 0;
transition: opacity 300ms;
}
</style>
<div
h-show="open"
h-transition-enter="opacity-enter"
h-transition-leave="opacity-leave"
>
Smoothly fading hamster content
</div>hamsterio transitions work perfectly with Tailwind, Animate.css, or any CSS framework:
<!-- Tailwind classes -->
<div
h-show="open"
h-transition-enter="transition ease-out duration-300 opacity-100 scale-100"
h-transition-leave="transition ease-in duration-200 opacity-0 scale-95"
>
Tailwind-powered hamster
</div>To prevent elements from briefly appearing before hamsterio loads, use inline styling: style="display: none;":
<div style="display: none;" h-show="open" h-transition-enter="fade-in">
<!-- Hidden until hamsterio initialises, no flash! -->
πΉ No premature hamster sightings
</div>hamsterio automatically removes the display: none attribute during initialisation, then h-show takes over.
Note: Transitions work seamlessly with flexbox, grid, and any display type. hamsterio remembers your element's original display value! π―
<style>
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-10px);
}
}
.slide-down { animation: slideDown 200ms ease-out; }
.slide-up { animation: slideUp 150ms ease-in; }
.dropdown { display: flex; fleh-direction: column; }
</style>
<div h-data="{ open: false }">
<button h-on:click="open = !open">
πΉ Hamster Menu
</button>
<div
h-show="open"
h-transition-enter="slide-down"
h-transition-leave="slide-up"
class="dropdown"
style="display: none;"
>
<a href="#">Feed hamster</a>
<a href="#">Pet hamster</a>
<a href="#">Give hamster a wheel</a>
</div>
</div><div
h-data="{
email: '',
isValid() {
return this.email.includes('@') && this.email.includes('hamster')
}
}"
>
<input
h-bind:value="email"
h-on:input="email = $event.target.value"
h-bind:class="{ 'border-red-500': email && !isValid() }"
class="border"
placeholder="[email protected]"
/>
<span h-show="email && !isValid()">Hamsters need valid emails!</span>
</div><div
h-data="{
name: '',
email: '',
submitting: false,
success: false,
async submit(e) {
e.preventDefault();
this.submitting = true;
const response = await fetch('/api/hamster-signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: this.name, email: this.email })
});
this.success = response.ok;
this.submitting = false;
}
}"
>
<form h-on:submit="await submit($event)">
<input
h-bind:value="name"
h-on:input="name = $event.target.value"
placeholder="Hamster name"
/>
<input
h-bind:value="email"
h-on:input="email = $event.target.value"
placeholder="[email protected]"
/>
<button
type="submit"
h-bind:disabled="submitting"
>
<span h-show="!submitting">Join the colony πΉ</span>
<span h-show="submitting">Scurrying to server...</span>
</button>
</form>
<div h-show="success">Welcome to the hamster family!</div>
</div><div
h-data="{
content: '<h2>πΉ Hamster Care Guide</h2><p>Feed your hamster <strong>twice daily</strong> with fresh seeds and vegetables.</p>'
}"
>
<div h-html="content"></div>
<button h-on:click="content = '<p>Content updated! Your hamster is happy! π</p>'">
Update Guide
</button>
</div><div
h-data="{
activeTab: 'home',
setTab(tab) { this.activeTab = tab }
}"
>
<button
h-on:click="setTab('home')"
h-bind:class="{ 'bg-hamster-blue': activeTab === 'home' }"
>
π Home Cage
</button>
<button
h-on:click="setTab('wheel')"
h-bind:class="{ 'bg-hamster-blue': activeTab === 'wheel' }"
>
βοΈ Exercise Wheel
</button>
<button
h-on:click="setTab('food')"
h-bind:class="{ 'bg-hamster-blue': activeTab === 'food' }"
>
π₯ Food Stash
</button>
<div h-show="activeTab === 'home'" h-text="'Welcome to the hamster home!'"></div>
<div h-show="activeTab === 'wheel'" h-text="'Time to run in circles!'"></div>
<div h-show="activeTab === 'food'" h-text="'Cheeks full of seeds π»'"></div>
</div><div
h-data="{
posts: [],
loading: true,
error: null
}"
h-init="
try {
const response = await fetch('/api/hamster_news?limit=5');
posts = await response.json();
} catch (e) {
error = 'Failed to fetch hamster news π’';
} finally {
loading = false;
}
"
>
<div h-show="loading">πΉ Hamster is fetching data...</div>
<div h-show="error" h-text="error"></div>
<ul h-show="!loading && !error">
<template h-for="post in posts">
<li>
<h3 h-text="post.title"></h3>
<p h-text="post.body"></p>
</li>
</template>
</ul>
</div>Under the hood, hamsterio uses signals - a reactive primitive that's simpler than your hamster's exercise routine.
import { createSignal, createEffect } from 'hamsterio';
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log('Count is:', count());
});
setCount(5); // Logs: "Count is: 5"Signals automatically track dependencies and only update what's necessary. It's like your hamster knowing exactly which food pellet changed.
Adding hamsters (elements) after page load? Use init(). Need to remove them cleanly? Use cleanup():
// Adding new content
const div = document.createElement('div');
div.setAttribute('h-data', '{ happy: true }');
document.body.appendChild(div);
hamsterio.init(div);
// Cleaning up before removal (prevents memory leaks!)
hamsterio.cleanup(div);
div.remove();Your hamster is tidy and doesn't like memory leaks! When you remove elements with h-data, always call cleanup() first to:
- π§Ή Remove event listeners (no ghost clicks!)
- π§Ή Dispose reactive effects (no phantom updates!)
- π§Ή Free up memory (more room for hamster snacks!)
Good hamster practices:
// β
Clean hamster - proper cleanup
const modal = document.querySelector('#hamster-modal');
hamsterio.cleanup(modal);
modal.remove();
// β
Re-initialising? Clean first!
const component = document.querySelector('[h-data]');
hamsterio.cleanup(component); // Clear old effects
hamsterio.init(component); // Set up fresh ones
// β Messy hamster - memory leak city!
document.querySelector('#dirty-modal').remove(); // Event listeners still attached! π±Note: h-for automatically calls cleanup() on its rendered items when the list changes, so you don't need to worry about that. Your hamster has your back! πΉ
Need to update data from outside (like reaching into the hamster cage)? Use getData():
const el = document.querySelector('[h-data]');
const data = hamsterio.getData(el);
data.count = 42; // Reactively updates! The hamster notices immediately.Use cases:
- Integration with third-party libraries (teaching old hamsters new tricks)
- External form handling (hamster data entry)
- Unit testing (making sure your hamster behaves)
- Console debugging (
console.log(hamsterio.getData(el))- peek at the hamster)
Example - Plotly chart integration:
const chart = document.getElementById('hamster-activity-chart');
chart.on('plotly_click', (data) => {
const hamsterData = hamsterio.getData(document.getElementById('stats'));
hamsterData.selectedDay = data.points[0].x;
hamsterData.wheelRotations = data.points[0].y;
});Works in all modern browsers (anything that understands WeakMap, Proxy, and the concept of a hamster).
| Framework | Size (min + gzip) |
|---|---|
| hamsterio | ~3KB πΉ |
| Alpine.js | ~15KB ποΈ |
| Vue.js | ~40KB π» |
| React | ~45KB ποΈποΈ |
Your hamster is judging your bundle size.
- Uses
new Function()andwithstatements for expression evaluation (keep user input sanitised, or your hamster might escape) h-htmlcan be dangerous with unsanitised user input - your hamster doesn't want XSS in its cage!- No virtual DOM diffing - this is by design for simplicity
- Doesn't include every Alpine.js feature (we're a hamster, not a capybara)
Found a bug? Want to add features? Your hamster wheel contributions are welcome!
- Fork the repo
- Create a feature branch (
git checkout -b feature/faster-hamster) - Commit your changes (
git commit -am 'Make hamster go zoom') - Push to the branch (
git push origin feature/faster-hamster) - Open a Pull Request
- Methods in
h-data -
h-binddirective (attribute binding) - Transition support
-
h-initdirective (hook into element initialisation) - Async/await support in
h-onandh-init -
h-htmldirective (inner HTML binding) - Proper cleanup system
- Event modifiers (
.prevent,.stop,.once) - Benchmarks
- Even more hamster emojis
hamsterio believes in:
- Simplicity over complexity - Like a hamster wheel, not a Rube Goldberg machine.
- HTML-first - Your markup should read like English, not assembly code.
- Minimal abstractions - Signals are simple. Keep it that way.
- Fast enough - Your users won't wait for your JavaScript hamster to wake up.
- Clean cages - Proper cleanup means no memory leaks. A tidy hamster is a happy hamster!
MIT - Free as a hamster running in an open field
Inspired by the brilliant work of:
Built by Bobby Donev
Remember: With great reactivity comes great responsibility. Use your hamster powers wisely. πΉβ¨