Version: 1.0.0 A guide for Alpine.js developers transitioning to Eleva
This guide helps Alpine.js developers understand Eleva by mapping familiar Alpine concepts to their Eleva equivalents. Both frameworks share a similar philosophy—lightweight, no build step, progressive enhancement—but take different approaches: Alpine is HTML-first with directives, while Eleva is JS-first with template functions.
| Alpine.js | Eleva | Notes |
|---|---|---|
x-data="{ count: 0 }" |
setup({ signal }) |
Component state |
x-text="message" |
${ctx.message.value} |
Text interpolation |
x-html="content" |
${ctx.content.value} |
HTML content |
x-show="isVisible" |
style="display: ${...}" |
Toggle visibility |
x-if="condition" |
${cond ? '...' : ''} |
Conditional render |
x-for="item in items" |
${items.map(...).join('')} |
List rendering |
x-on:click="handler" |
@click="handler" |
Event handling |
x-model="value" |
value + @input |
Two-way binding |
x-bind:class="..." |
class="${...}" |
Attribute binding |
x-init="..." |
Code in setup() |
Initialization |
$watch('prop', fn) |
signal.watch(fn) |
Watch changes |
$store |
Store plugin | Global state |
<!-- Logic lives in HTML attributes -->
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Content</div>
</div>
// Logic lives in JavaScript, HTML is generated
const Toggle = {
setup({ signal }) {
const open = signal(false);
return { open, toggle: () => open.value = !open.value };
},
template: (ctx) => `
<div>
<button @click="toggle">Toggle</button>
${ctx.open.value ? '<div>Content</div>' : ''}
</div>
`
};
Neither approach is “better”—they serve different preferences:
Alpine:
<div x-data="{ count: 0, name: 'John' }">
<p x-text="count"></p>
<p x-text="name"></p>
<button @click="count++">Increment</button>
</div>
Eleva:
const Counter = {
setup({ signal }) {
const count = signal(0);
const name = signal('John');
const increment = () => count.value++;
return { count, name, increment };
},
template: (ctx) => `
<div>
<p>${ctx.count.value}</p>
<p>${ctx.name.value}</p>
<button @click="increment">Increment</button>
</div>
`
};
Key differences:
signal() in setup().value propertyAlpine:
<div x-data="{ message: 'Hello', html: '<strong>Bold</strong>' }">
<span x-text="message"></span>
<span x-html="html"></span>
<p x-text="`Count is ${count}`"></p>
</div>
Eleva:
template: (ctx) => `
<div>
<span>${ctx.message.value}</span>
<span>${ctx.html.value}</span>
<p>Count is ${ctx.count.value}</p>
</div>
`
Key differences:
Alpine:
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open">Visible when open</div>
<div x-show="open" x-transition>With transition</div>
</div>
Eleva:
const Toggle = {
setup({ signal }) {
const open = signal(false);
return { open, toggle: () => open.value = !open.value };
},
template: (ctx) => `
<div>
<button @click="toggle">Toggle</button>
<div style="${ctx.open.value ? '' : 'display: none;'}">
Visible when open
</div>
<div
style="${ctx.open.value ? '' : 'display: none;'}"
class="${ctx.open.value ? 'fade-in' : 'fade-out'}"
>
With transition (use CSS classes)
</div>
</div>
`
};
Key differences:
x-show handles display toggling automaticallyAlpine:
<div x-data="{ loggedIn: false, role: 'admin' }">
<template x-if="loggedIn">
<div>Welcome back!</div>
</template>
<template x-if="role === 'admin'">
<div>Admin panel</div>
</template>
<template x-if="role === 'user'">
<div>User dashboard</div>
</template>
</div>
Eleva:
const Dashboard = {
setup({ signal }) {
const loggedIn = signal(false);
const role = signal('admin');
return { loggedIn, role };
},
template: (ctx) => `
<div>
${ctx.loggedIn.value ? `
<div>Welcome back!</div>
` : ''}
${ctx.role.value === 'admin' ? `
<div>Admin panel</div>
` : ctx.role.value === 'user' ? `
<div>User dashboard</div>
` : ''}
</div>
`
};
Key differences:
<template x-if> for conditional blocksx-if removes/adds DOM elements; Eleva re-rendersAlpine:
<div x-data="{ items: [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]}">
<ul>
<template x-for="item in items" :key="item.id">
<li x-text="item.name"></li>
</template>
</ul>
</div>
Eleva:
const ItemList = {
setup({ signal }) {
const items = signal([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]);
return { items };
},
template: (ctx) => `
<div>
<ul>
${ctx.items.value.map(item => `
<li key="${item.id}">${item.name}</li>
`).join('')}
</ul>
</div>
`
};
Key differences:
x-for directive with <template> wrapper.map().join(''):key for efficient updates.join('') to convert array to stringAlpine:
<div x-data="{ count: 0 }">
<!-- Full syntax -->
<button x-on:click="count++">+</button>
<!-- Shorthand -->
<button @click="count++">+</button>
<!-- With modifiers -->
<form @submit.prevent="handleSubmit">
<button @click.stop="doSomething">Click</button>
<input @keydown.enter="submit" />
<div @click.outside="close">Dropdown</div>
</div>
Eleva:
const Events = {
setup({ signal }) {
const count = signal(0);
const handleSubmit = (e) => {
e.preventDefault();
// submit logic
};
const doSomething = (e) => {
e.stopPropagation();
// logic
};
const submit = () => { /* submit */ };
return { count, handleSubmit, doSomething, submit };
},
template: (ctx) => `
<div>
<button @click="() => count.value++">+</button>
<button @click="() => count.value++">+</button>
<form @submit="handleSubmit">
<button @click="doSomething">Click</button>
<input @keydown="(e) => e.key === 'Enter' && submit()" />
<!-- For click.outside, implement custom logic -->
</div>
`
};
Key differences:
@event shorthand syntax.prevent, .stop, .outside)Alpine:
<div x-data="{ text: '', checked: false, selected: 'a' }">
<input type="text" x-model="text" />
<input type="checkbox" x-model="checked" />
<select x-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
<!-- With modifiers -->
<input x-model.number="count" />
<input x-model.debounce.500ms="search" />
</div>
Eleva:
const Form = {
setup({ signal }) {
const text = signal('');
const checked = signal(false);
const selected = signal('a');
const count = signal(0);
const search = signal('');
// Debounce helper
let timeout;
const debouncedSearch = (value) => {
clearTimeout(timeout);
timeout = setTimeout(() => search.value = value, 500);
};
return { text, checked, selected, count, search, debouncedSearch };
},
template: (ctx) => `
<div>
<input
type="text"
value="${ctx.text.value}"
@input="(e) => text.value = e.target.value"
/>
<input
type="checkbox"
${ctx.checked.value ? 'checked' : ''}
@change="(e) => checked.value = e.target.checked"
/>
<select @change="(e) => selected.value = e.target.value">
<option value="a" ${ctx.selected.value === 'a' ? 'selected' : ''}>A</option>
<option value="b" ${ctx.selected.value === 'b' ? 'selected' : ''}>B</option>
</select>
<input
type="number"
value="${ctx.count.value}"
@input="(e) => count.value = Number(e.target.value)"
/>
<input
value="${ctx.search.value}"
@input="(e) => debouncedSearch(e.target.value)"
/>
</div>
`
};
Key differences:
x-model handles two-way binding automaticallyAlpine:
<div x-data="{ imageUrl: '/img.jpg', isActive: true }">
<img x-bind:src="imageUrl" />
<img :src="imageUrl" />
<div :class="{ active: isActive, 'text-red': hasError }"></div>
<div :class="isActive ? 'active' : 'inactive'"></div>
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
</div>
Eleva:
template: (ctx) => `
<div>
<img src="${ctx.imageUrl.value}" />
<img src="${ctx.imageUrl.value}" />
<div class="${ctx.isActive.value ? 'active' : ''} ${ctx.hasError.value ? 'text-red' : ''}"></div>
<div class="${ctx.isActive.value ? 'active' : 'inactive'}"></div>
<div style="color: ${ctx.textColor.value}; font-size: ${ctx.size.value}px;"></div>
</div>
`
Key differences:
:attr shorthand for x-bind:attrAlpine:
<div
x-data="{ users: [] }"
x-init="users = await (await fetch('/api/users')).json()"
>
<template x-for="user in users" :key="user.id">
<div x-text="user.name"></div>
</template>
</div>
Eleva:
const UserList = {
setup({ signal }) {
const users = signal([]);
// Runs on mount (like x-init)
fetch('/api/users')
.then(res => res.json())
.then(data => users.value = data);
return { users };
},
template: (ctx) => `
<div>
${ctx.users.value.map(user => `
<div key="${user.id}">${user.name}</div>
`).join('')}
</div>
`
};
Alpine:
<div
x-data="{ query: '', results: [] }"
x-init="$watch('query', async (value) => {
if (value.length > 2) {
results = await search(value);
}
})"
>
<input x-model="query" />
</div>
Eleva:
const Search = {
setup({ signal }) {
const query = signal('');
const results = signal([]);
query.watch(async (value) => {
if (value.length > 2) {
results.value = await search(value);
}
});
return { query, results };
},
template: (ctx) => `
<div>
<input
value="${ctx.query.value}"
@input="(e) => query.value = e.target.value"
/>
</div>
`
};
Alpine:
// Define store
Alpine.store('user', {
name: 'John',
loggedIn: false,
login() {
this.loggedIn = true;
}
});
<!-- Use in component -->
<div x-data>
<span x-text="$store.user.name"></span>
<button @click="$store.user.login()">Login</button>
</div>
Eleva:
import Eleva from "eleva";
import { Store } from "eleva/plugins";
const app = new Eleva("App");
app.use(Store, {
state: {
user: {
name: 'John',
loggedIn: false
}
},
actions: {
login: (state) => {
state.user.value = { ...state.user.value, loggedIn: true };
}
}
});
// In component
const UserStatus = {
setup({ store }) {
return {
user: store.state.user,
login: () => store.dispatch('login')
};
},
template: (ctx) => `
<div>
<span>${ctx.user.value.name}</span>
<button @click="login">Login</button>
</div>
`
};
| Alpine Magic | Eleva Equivalent |
|---|---|
$el |
Access via setup() or DOM queries |
$refs |
Use id or class selectors |
$store |
store from setup({ store }) |
$watch |
signal.watch() |
$dispatch |
Custom event emitter or props |
$nextTick |
queueMicrotask() |
$root |
Parent component reference |
$data |
ctx in template |
$id |
Generate IDs manually |
<div x-data="dropdown">
<button @click="toggle">Menu</button>
<div x-show="open" @click.outside="close">
<a href="#">Item 1</a>
<a href="#">Item 2</a>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('dropdown', () => ({
open: false,
toggle() { this.open = !this.open; },
close() { this.open = false; }
}));
});
</script>
app.component("Dropdown", {
setup({ signal }) {
const open = signal(false);
const toggle = () => open.value = !open.value;
const close = () => open.value = false;
// Handle click outside
const handleClickOutside = (e) => {
if (open.value && !e.target.closest('.dropdown')) {
close();
}
};
document.addEventListener('click', handleClickOutside);
return {
open,
toggle,
close,
onUnmount: () => document.removeEventListener('click', handleClickOutside)
};
},
template: (ctx) => `
<div class="dropdown">
<button @click="toggle">Menu</button>
${ctx.open.value ? `
<div class="dropdown-menu">
<a href="#">Item 1</a>
<a href="#">Item 2</a>
</div>
` : ''}
</div>
`
});
<div x-data="{
todos: [],
newTodo: '',
addTodo() {
if (!this.newTodo.trim()) return;
this.todos.push({
id: Date.now(),
text: this.newTodo,
done: false
});
this.newTodo = '';
},
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
}
}">
<form @submit.prevent="addTodo">
<input x-model="newTodo" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
<template x-for="todo in todos" :key="todo.id">
<li>
<input type="checkbox" x-model="todo.done" />
<span :class="{ 'line-through': todo.done }" x-text="todo.text"></span>
<button @click="removeTodo(todo.id)">×</button>
</li>
</template>
</ul>
</div>
app.component("TodoApp", {
setup({ signal }) {
const todos = signal([]);
const newTodo = signal('');
const addTodo = () => {
if (!newTodo.value.trim()) return;
todos.value = [...todos.value, {
id: Date.now(),
text: newTodo.value,
done: false
}];
newTodo.value = '';
};
const toggleTodo = (id) => {
todos.value = todos.value.map(t =>
t.id === id ? { ...t, done: !t.done } : t
);
};
const removeTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id);
};
return { todos, newTodo, addTodo, toggleTodo, removeTodo };
},
template: (ctx) => `
<div>
<form @submit="(e) => { e.preventDefault(); addTodo(); }">
<input
value="${ctx.newTodo.value}"
@input="(e) => newTodo.value = e.target.value"
placeholder="Add todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
${ctx.todos.value.map(todo => `
<li key="${todo.id}">
<input
type="checkbox"
${todo.done ? 'checked' : ''}
@change="() => toggleTodo(${todo.id})"
/>
<span class="${todo.done ? 'line-through' : ''}">${todo.text}</span>
<button @click="() => removeTodo(${todo.id})">×</button>
</li>
`).join('')}
</ul>
</div>
`
});
app.mount(document.getElementById("app"), "TodoApp");
.prevent, .stop, .outside, .debounce, etc.Choose Alpine when:
Choose Eleva when:
x-data with component setup() and signal()x-text/x-html to template interpolationx-show with inline style togglingx-if to ternary expressionsx-for with .map().join('')x-model to value + @input patternx-bind to direct attribute interpolationx-init logic into setup()$watch with signal.watch()Alpine.store with Eleva Store pluginkey attributes to all list items| ← From Vue | Back to Migration Overview | From jQuery → |