Migrating from Svelte
Svelte and WildflowerJS share a philosophy: direct mutation, minimal boilerplate, and high performance without a virtual DOM. The key difference is that Svelte requires a compiler to transform .svelte files into JavaScript, while WildflowerJS runs directly in the browser with plain HTML and standard JavaScript. No build step required.
array.push(), obj.prop = val), that mental model transfers directly to WildflowerJS. You won't need to unlearn anything, just learn a different way to express the same ideas.
Mental Model Shift
Before diving into syntax, understand the architectural differences:
| Concept | Svelte | WildflowerJS |
|---|---|---|
| Architecture | Compiler-based; .svelte files are compiled to optimized JS at build time |
Runtime-based; plain HTML + JS, no build step needed |
| Reactivity model | Runes ($state, $derived, $effect); compiler transforms assignments into reactive updates |
Object-based definitions (state, computed, init); proxy-based reactivity at runtime |
| Component format | Single-file components (.svelte) with <script>, markup, and <style> in one file |
HTML with data-component attributes + separate JS definitions |
| How reactivity works | Implicit; the compiler analyzes assignments and generates reactive code | Explicit proxies; the runtime intercepts property access and mutation automatically |
| Template syntax | Svelte-specific: {#if}, {#each}, {expression} |
Standard HTML: data-show, data-list, data-bind |
| Mutation | Direct mutation: array.push(), obj.prop = val |
Direct mutation: this.array.push(), this.prop = val |
Concept Mapping
A complete mapping from Svelte 5 concepts to WildflowerJS equivalents:
| Svelte 5 | WildflowerJS | Notes |
|---|---|---|
$state(value) |
state: { prop: value } |
Defined in the component object |
$derived(expr) |
computed: { prop() { return expr; } } |
Cached, auto-tracked dependencies |
$effect(() => {}) |
init() |
Runs once after mount; use watch for reactive side effects |
{expression} |
data-bind="prop" |
Binds text content to a state or computed property |
{@html content} |
data-bind-html="content" |
Renders raw HTML |
on:click={fn} |
data-action="fn" |
Click is the default event |
on:input={fn} |
data-action="input:fn" |
Prefix with event name for non-click events |
bind:value |
data-model |
Two-way binding for form inputs |
{#if cond} |
data-render="cond" |
Adds/removes from DOM (like Svelte's {#if}) |
{#each items as item (item.id)} |
data-list="items" data-key="id" + <template> |
Keyed list rendering with reconciliation |
class:active={isActive} |
data-bind-class="isActive ? 'active' : ''" |
Expression-based class binding |
style:color={val} |
data-bind-style="{ color: val }" |
Expression-based style binding |
Svelte stores (writable()) |
wildflower.store() |
Global reactive state with methods |
$store auto-subscription |
subscribe: { store: [...] } |
Declarative subscription with automatic cleanup |
onMount() |
init() |
Called after component mounts |
onDestroy() |
destroy() |
Called before component unmounts |
Side-by-Side: Todo List
A complete todo application in both frameworks, showing how the same functionality translates:
Svelte 5
<script>
let todos = $state([]);
let newTodo = $state('');
let remaining = $derived(
todos.filter(t => !t.done).length
);
function addTodo() {
if (!newTodo.trim()) return;
todos.push({
id: Date.now(),
text: newTodo,
done: false
});
newTodo = '';
}
function removeTodo(index) {
todos.splice(index, 1);
}
</script>
<div>
<h3>Todos ({remaining} remaining)</h3>
<input bind:value={newTodo}
on:keydown|enter={addTodo} />
<button on:click={addTodo}>Add</button>
<ul>
{#each todos as todo, index (todo.id)}
<li>
<input type="checkbox"
bind:checked={todo.done} />
<span style:text-decoration={
todo.done
? 'line-through'
: 'none'
}>
{todo.text}
</span>
<button on:click={
() => removeTodo(index)
}>×</button>
</li>
{/each}
</ul>
</div>
WildflowerJS
<div data-component="todo-app">
<h3>Todos (<span data-bind="remaining">0
</span> remaining)</h3>
<input data-model="newTodo"
data-action="keydown.enter:addTodo" />
<button data-action="addTodo">Add</button>
<ul data-list="todos" data-key="id">
<template>
<li>
<input type="checkbox"
data-model="done" />
<span data-bind="text"
data-bind-style="{ textDecoration: done ? 'line-through' : 'none' }"></span>
<button data-action="removeTodo">
×
</button>
</li>
</template>
</ul>
</div>
wildflower.component('todo-app', {
state: { todos: [], newTodo: '' },
computed: {
remaining() {
return this.todos.filter(
t => !t.done
).length;
}
},
addTodo() {
if (!this.newTodo.trim()) return;
this.todos.push({
id: Date.now(),
text: this.newTodo,
done: false
});
this.newTodo = '';
},
removeTodo(event, element, details) {
this.todos.splice(details.index, 1);
}
});
array.push() and array.splice() for mutations. Both use this.prop for state access. The logic is nearly identical; only the template syntax differs.
Pattern-by-Pattern Migration
Component Definition
Svelte uses single-file .svelte components that the compiler processes. WildflowerJS uses a data-component attribute on an HTML element paired with a JavaScript definition.
Svelte (Counter.svelte)
<script>
let count = $state(0);
function increment() { count++; }
</script>
<button on:click={increment}>
Count: {count}
</button>
WildflowerJS
<div data-component="counter">
<button data-action="increment">
Count: <span data-bind="count">0</span>
</button>
</div>
wildflower.component('counter', {
state: { count: 0 },
increment() { this.count++; }
});
Reactive State
Svelte 5 uses the $state rune to declare reactive variables. WildflowerJS defines state as a plain object in the component definition. Both support direct mutation.
Svelte 5
let name = $state('Alice');
let items = $state([]);
// Direct mutation works in both:
name = 'Bob';
items.push({ id: 1, label: 'New' });
WildflowerJS
wildflower.component('example', {
state: { name: 'Alice', items: [] },
changeName() {
// Direct mutation works the same way:
this.name = 'Bob';
this.items.push({ id: 1, label: 'New' });
}
});
Derived Values
Svelte's $derived rune becomes a computed property. Both are cached and automatically re-evaluate when dependencies change.
Svelte 5
let price = $state(100);
let quantity = $state(2);
let total = $derived(price * quantity);
let formatted = $derived(`$${total.toFixed(2)}`);
WildflowerJS
wildflower.component('cart', {
state: { price: 100, quantity: 2 },
computed: {
total() {
return this.price * this.quantity;
},
formatted() {
return `$${this.total.toFixed(2)}`;
}
}
});
Effects
Svelte's $effect runs reactive side effects whenever its dependencies change. WildflowerJS uses init() for mount-time setup and watch for reactive observation.
Svelte 5
$effect(() => {
console.log('count changed:', count);
});
$effect(() => {
document.title = `Count: ${count}`;
return () => {
// cleanup on re-run or destroy
};
});
WildflowerJS
wildflower.component('example', {
state: { count: 0 },
init() {
// Runs once after mount
console.log('mounted with count:', this.count);
},
// Watch specific properties for changes
watch: {
count(newVal, oldVal) {
document.title = `Count: ${newVal}`;
}
},
destroy() {
// Cleanup on unmount
}
});
Event Handling
Svelte uses on:event syntax (or onevent in Svelte 5). WildflowerJS uses data-action with an optional event prefix.
Svelte
<!-- Click (default) -->
<button on:click={handleClick}>Click</button>
<!-- Other events -->
<input on:input={handleInput} />
<form on:submit|preventDefault={handleSubmit}>
<!-- Keyboard -->
<input on:keydown={handleKey} />
<!-- Inline handler -->
<button on:click={() => count++}>+1</button>
WildflowerJS
<!-- Click (default) -->
<button data-action="handleClick">Click</button>
<!-- Other events -->
<input data-action="input:handleInput" />
<form data-action="submit:handleSubmit">
<!-- Keyboard -->
<input data-action="keydown.enter:handleKey" />
<!-- No inline handlers; define methods -->
<button data-action="increment">+1</button>
Conditional Rendering
Svelte uses {#if}/{:else} blocks. WildflowerJS uses data-show (CSS toggle) or data-render (DOM insertion/removal).
Svelte
{#if isLoggedIn}
<p>Welcome, {username}!</p>
{:else}
<p>Please log in.</p>
{/if}
{#if items.length > 0}
<span class="badge">{items.length}</span>
{/if}
WildflowerJS
<!-- data-render adds/removes from DOM -->
<p data-render="isLoggedIn">
Welcome, <span data-bind="username"></span>!
</p>
<p data-render="!isLoggedIn">
Please log in.
</p>
<!-- data-show toggles display:none -->
<span class="badge"
data-show="items.length > 0"
data-bind="items.length"></span>
data-show for frequent toggles (like Svelte's CSS-based hiding). Use data-render for expensive content that should be fully removed from the DOM (like Svelte's {#if}).
List Rendering
Svelte uses {#each} blocks with optional keying. WildflowerJS uses data-list with a <template> child element.
Svelte
{#each users as user (user.id)}
<div class="card">
<h4>{user.name}</h4>
<p>{user.email}</p>
<button on:click={
() => removeUser(user.id)
}>Delete</button>
</div>
{:else}
<p>No users found.</p>
{/each}
WildflowerJS
<div data-list="users" data-key="id">
<template>
<div class="card">
<h4 data-bind="name"></h4>
<p data-bind="email"></p>
<button data-action="removeUser">
Delete
</button>
</div>
</template>
</div>
<!-- Empty state: use data-show -->
<p data-show="users.length === 0">
No users found.
</p>
Forms
Svelte's bind:value maps directly to data-model. Both provide two-way binding for form inputs.
Svelte
<input bind:value={name} />
<textarea bind:value={bio}></textarea>
<select bind:value={color}>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<input type="checkbox"
bind:checked={agreed} />
WildflowerJS
<input data-model="name" />
<textarea data-model="bio"></textarea>
<select data-model="color">
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<input type="checkbox"
data-model="agreed" />
Stores
Svelte's built-in stores (writable, readable) become WildflowerJS stores. The $store auto-subscription syntax becomes declarative subscribe blocks.
Svelte
// store.js
import { writable, derived } from 'svelte/store';
export const cart = writable([]);
export const total = derived(cart, $cart =>
$cart.reduce((sum, i) => sum + i.price, 0)
);
// In component:
import { cart, total } from './store.js';
// $cart auto-subscribes
{$total}
$cart.push(item); // Svelte 5 with $state
WildflowerJS
// Store definition
wildflower.store('cart', {
state: { items: [] },
computed: {
total() {
return this.items.reduce(
(sum, i) => sum + i.price, 0
);
}
},
addItem(item) {
this.items.push(item);
}
});
// In component:
wildflower.component('checkout', {
subscribe: { cart: ['items', 'total'] },
// Access: this.stores.cart.total
});
$ prefix: data-bind="$cart.total". This is similar to Svelte's $store syntax.
Common Gotchas
-
No compiler needed. Just include a
<script>tag pointing to the WildflowerJS distribution file. No SvelteKit, no Vite plugin, nosvelte.config.js, no preprocessors. -
No
$stateor$derived. Usestate: {}andcomputed: {}in the component definition object. Reactivity comes from the proxy system, not compiler transforms. -
No
{expression}in markup. Usedata-bind="prop"on an element instead. Text interpolation with curly braces is not supported. Every bound value needs its own element (typically a<span>). -
No
{#each}block syntax. Usedata-liston a container element with a<template>child. The template defines what each item looks like. -
State is scoped to the component. No file-level variable declarations like
let count = $state(0). All state lives in thestateobject and is accessed viathis. -
Direct mutation still works. Both frameworks support
array.push(),array.splice(), andobj.prop = val. This is one thing that does not need to change.
Migration Checklist
A step-by-step process for migrating a Svelte application to WildflowerJS:
-
Remove the Svelte/SvelteKit toolchain. Uninstall
@sveltejs/kit,svelte,@sveltejs/adapter-*, and related Vite plugins. Removesvelte.config.js. -
Convert
.sveltefiles to HTML + JS. Extract the<script>into a.jsfile withwildflower.component(). Convert the markup to usedata-*attributes. Move styles to a CSS file. -
Replace
$state/$derived/$effect. Convert runes tostate,computed, andinit/watchin the component definition. -
Replace Svelte template syntax with
data-*attributes. Convert{expression}todata-bind,{#if}todata-render/data-show,{#each}todata-list,bind:valuetodata-model, andon:eventtodata-action. -
Convert Svelte stores to
wildflower.store(). Replacewritable()/readable()/derived()with WildflowerJS store definitions. Replace$storeauto-subscriptions withsubscribe: {}blocks. -
Replace SvelteKit routing with RouteManager. Convert file-based routes to WildflowerJS route definitions. Replace
<a href>withdata-routelinks. -
Remove all build configuration. No
vite.config.js, no build scripts. Just include the WildflowerJS script tag and serve static files. - Test each component. Verify state updates, computed properties, list rendering, event handling, and store subscriptions work correctly in the browser.