WildflowerJS Reactive JS, No BS*

A no-build reactive JavaScript framework, rooted in the web platform.
No build step. No dependencies. No lock-in.

<script src="wildflower.min.js"></script> ...and start building.

Back to Basics

The code you write is 100% web standard code. HTML stays HTML. JavaScript stays JavaScript. CSS stays CSS. No JSX, no templating language, no custom syntax to learn. If you know the web platform, you already know how to use this.

WildflowerJS extends the web platform. It doesn't replace it.

Your Development Simplified

Because you develop with 100% web standards, every tool in your existing chain already understands the code: IDE, browser DevTools, linter, formatter, screen reader, SEO crawler. Nothing to install, no custom file types, no sourcemaps. Save the file, refresh, and your change is live.

Just be a web developer.

Batteries Included: One Mental Model

Router, SSR, stores, computed properties, two-way binding, event modifiers, data pools, and TypeScript types, all built in, all speaking the same language. Learn data-bind once and you know binding everywhere: lists, pools, stores, forms. There's no five-library stack to keep in sync.

One script tag. Everything you need.

<div data-component="counter">
  <span data-bind="count"></span>
  <button data-action="increment">
    +1
  </button>
</div>

<script>
wildflower.component('counter', {
  state: { count: 0 },
  increment() { this.count++ }
})
</script>

How It Works

data-bind connects state to the DOM.

data-action connects events to methods.

this.count++ triggers a precise DOM update.

Mutate state. The DOM updates.

Two Reactivity Modes

data-list for automatic reactivity: mutate state, DOM updates. data-pool for explicit control: plain objects, zero proxy overhead, you say what changed.

Same template syntax. Different performance profile. From interactive forms to per-frame particle systems. You choose the right tradeoff for the job.

Try it. Right-click, inspect this demo. Every dot is a real DOM element.

See full demo →

* Build Step

Zero Toolchain

Modern frameworks ask you to install a compiler, a bundler, a package manager, hundreds of fragile transitive dependencies, and a framework-specific file format, before you write a single line of your application.

WildflowerJS was built starting from a single principle: no build step, no tooling. Ever.

WildflowerJS asks you to add a script tag.

There's no CLI scaffolding step, no config files, no .vue/.jsx/.svelte source format. You don't debug through sourcemaps or wait on a build pipeline. Your project has zero dependencies.

Performance isn't a tradeoff. Build steps optimize bundle delivery, not the runtime work that follows it. WildflowerJS writes directly to the DOM, with no virtual DOM or reconciliation pass between state change and update, so it doesn't need a build step to be fast.

The framework is full-featured without the toolchain: router, SSR, stores, computed properties, transitions, pools. You don't need a toolchain to use any of it.

my-app/
  index.html
  app.js
  style.css
  wildflower.min.js

That's the entire project. No package.json.
No node_modules. No config files. Ship it.

Zero Install. Zero Attack Surface.

Every dependency you install is trust extended to a maintainer you've never met, running scripts on your dev machine and in your CI. A typical React + Vite + UI‑lib setup pulls in 300+ transitive packages before you write a feature.

Each one is a potential intrusion vector. NPM worms, OAuth chains compromising deploy platforms, postinstall hijacking: the supply chain is now where production code gets compromised, not the deploy. And signing isn't a backstop: Mini Shai‑Hulud (May 2026) compromised 170+ packages whose malicious versions carried valid SLSA Build Level 3 provenance, because the attestation came from build infrastructure the worm had already taken over.

WildflowerJS users don't have this attack surface, by construction. There is no npm install, no postinstall script, no transitive package graph. The framework is one file you copy or pin by hash.

As of v1.1, the same holds for building the framework itself. WildflowerJS bundles with a vendored rollup and terser pipeline pulled as three SHA‑512‑pinned tarballs: no npm install, no transitive packages, no postinstall scripts in the build path. The entire toolchain is three files you verify by hash.

Zero dependencies is the absence of a problem the rest of the industry has not properly addressed.

A typical React/Vue project:

  npm install
  ├── hundreds of packages
  ├── from hundreds of maintainers
  ├── postinstall scripts run on install
  └── tens to hundreds of MB of transitive code

WildflowerJS:

  <script src="wildflower.min.js"></script>
  └── 1 file.
      No transitive dependencies.

Zero Lock-in

WildflowerJS works with the DOM, not instead of it. There's no virtual DOM intercepting your code and no compiler rewriting your markup. The render cycle is yours.

That means Leaflet, DataTables, Chart.js, D3, Three.js, any library that touches the DOM, just works. No wrapper packages or framework-specific escape hatches required. Drop in a script tag and use it.

Because your code is standard HTML and JavaScript, you're never locked in. Your skills transfer and your code is more portable. If you outgrow the framework, your knowledge doesn't expire.

This also means your "ecosystem" is all of the world of vanilla JS. Without compromises or hacks.

<!-- Use any library directly -->
<div data-component="map-view">
  <div id="map" style="height: 400px"></div>
</div>
wildflower.component('map-view', {
  state: { lat: 51.505, lng: -0.09 },
  init() {
    // Leaflet works as-is. No wrappers.
    this._map = L.map('map')
      .setView([this.lat, this.lng], 13);
    L.tileLayer('https://{s}.tile.osm.org'
      + '/{z}/{x}/{y}.png').addTo(this._map);
  }
})

Precise Reactivity

When you write this.count++, WildflowerJS updates the single DOM node bound to count. Nothing else is touched. There's no tree diffing or reconciliation pass to figure that out.

This isn't a tradeoff. You get fine-grained updates and a simple mental model. Change a property, the bound element updates. That's the entire reactivity model.

Other frameworks ask you to learn signals, accessors, memos, effects, and subscription lifecycles to achieve what WildflowerJS does with a property assignment.

wildflower.component('dashboard', {
  state: {
    users: 1420,
    status: 'healthy'
  },
  computed: {
    summary() {
      return this.users + ' users, ' + this.status;
    }
  },
  refresh() {
    this.users = 1421;
    // Only the elements bound to 'users'
    // and 'summary' update. Everything
    // else on the page is untouched.
  }
})

One Reactivity Model. Everywhere.

Components, Stores, and Plugins all share the same reactive foundation. State, computed properties, and methods work identically no matter where they live. Learn it once, it works the same way in a UI component, a global store, or a framework plugin.

Other frameworks make you learn a different system for each layer. React components use hooks, but stores need Redux or Zustand, which are completely different APIs. Vue components use reactive data, but Pinia stores have their own patterns. Every layer is a new mental model.

In WildflowerJS, there's one model. A store is a component without a template. A plugin is an entity that extends the framework itself, adding directives, lifecycle hooks, and services. The same this.count++ triggers the same reactivity everywhere.

This unlocks patterns other frameworks can't express. A store can run headless physics simulations with tick(), feeding data into a component that renders it through a pool, all using the same reactive primitives, no glue code required.

// Component: reactive UI
wildflower.component('cart', {
  state: { items: [] },
  computed: {
    total() { return this.items.length; }
  }
})

// Store: global shared state
wildflower.store('user', {
  state: { name: '', role: 'guest' },
  computed: {
    isAdmin() { return this.role === 'admin'; }
  }
})

// Plugin: extends the framework
wildflower.plugin({
  name: 'notifications',
  state: { items: [], unreadCount: 0 },
  computed: {
    hasUnread() { return this.unreadCount > 0; }
  },
  add(msg) { this.items.push(msg); this.unreadCount++; }
})
// Access globally: wildflower.$notifications.add(...)

// Same state. Same computed. Same methods.

Data Pools

Every framework wraps collection items in reactive proxies, whether the item needs it or not. WildflowerJS gives you a choice: data-list for push reactivity (automatic), data-pool for pull reactivity (explicit control, zero proxy overhead).

Pools render plain objects with the same template syntax as lists. Mutate the object, call markDirty(), and only that item updates. Full CRUD, selection, bulk operations, all faster than the push-reactive path.

And because pools use pull-based rendering, they scale to simulations, games, particle systems, and data visualizations at native frame rate. Use cases that would choke a virtual DOM. No other framework has anything like this.

<div data-component="user-table">
  <tbody data-pool="users" data-key="id">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="status"
            data-bind-class="status === 'active'
              ? 'badge success'
              : 'badge inactive'"></td>
      </tr>
    </template>
  </tbody>
</div>
wildflower.component('user-table', {
  pools: { users: {} },

  init() {
    // Populate: plain objects, no proxies
    data.forEach(u => this.pools.users.add(u));
  },

  // Optional: add tick() and the same pool
  // renders every frame. Same template, same
  // data, different rendering frequency.
  // That's the only difference between a
  // display table and a particle system.
})

Built for AI-Assisted Development

Because WildflowerJS is standard HTML and JavaScript, AI code assistants already know how to write it. There's no custom syntax to hallucinate or compiler quirks to work around. The code an AI generates runs exactly as written, with no build step between generation and execution.

We go further. WildflowerJS ships an AI-optimized reference page with patterns, anti-patterns, and examples designed for code generation context windows. Our llms.txt file follows the llms.txt convention for machine-readable documentation.

And for structured app generation, our Universal App Manifest lets you describe an entire application as a JSON schema (components, state, computed properties, methods, templates) and have an AI generate the working code from the manifest, mediated through framework-specific idiom files.

You: "Build me a todo app with
WildflowerJS"

AI reads llms.txt or ai-assistant.html
     ↓
Generates standard HTML + JS
     ↓
<div data-component="todo-app">
  <input data-model="newItem">
  <button data-action="addItem">
    Add
  </button>
  <ul data-list="items">
    <template>
      <li data-bind="text"></li>
    </template>
  </ul>
</div>
     ↓
Open in your browser. It works, and you can read and understand the code.

Migrating from Alpine.js

Alpine.js is WildflowerJS's closest relative. Both are attribute-based, no-build-step frameworks that enhance HTML with reactive behavior. WildflowerJS adds proper component lifecycle, stores with subscriptions, cached computed properties, keyed list reconciliation, and scales better for larger applications.

Good news: If you know Alpine, you already understand the WildflowerJS philosophy. The migration is mostly about moving inline logic into structured component definitions and swapping x-* prefixes for data-* equivalents. Your HTML structure stays remarkably similar.

Mental Model Shift

Both frameworks share the same core idea: enhance HTML with attributes instead of replacing it with JSX or custom file formats. But they diverge in how they organize logic and scale.

Concept Alpine.js WildflowerJS
Philosophy "Sprinkle" interactivity on existing pages Scales from sprinkles to full SPAs
State & logic Inline in x-data attribute Separate JS definitions (clean separation of concerns)
Getters / computed Re-evaluate on every access Cached, only recalculates when dependencies change
List rendering x-for with template expressions data-list with keyed reconciliation
Template element Uses <template> Uses <template> (same concept)
Component reuse Alpine.data() or inline Named components with full lifecycle
Global state Alpine.store() wildflower.store() with subscriptions
Scaling path Limited, designed for enhancement Full SPA architecture with routing, nested components, SSR
The biggest win: Both frameworks bind with attributes and use <template> elements, so the HTML structure of your Alpine components translates almost directly to WildflowerJS. The main work is extracting inline JavaScript into component definitions.

Attribute Mapping

A direct translation table for every Alpine directive:

Alpine.js WildflowerJS Notes
x-data="{ state }" data-component="name" State defined in JS, not inline
x-text="expr" data-bind="prop" Text content binding
x-html="expr" data-bind-html="prop" Raw HTML binding
@click="expr" data-action="method" Logic moves to named JS methods
@click.prevent data-action="method" Event is passed to method; call event.preventDefault() there
@keydown.enter="expr" data-action="keydown.enter:method" Event modifiers use event.key:method syntax
x-model="prop" data-model="prop" Two-way binding, nearly identical
x-show="cond" data-show="cond" CSS display toggle, nearly identical
x-if="cond" data-render="cond" DOM insertion/removal
x-for="item in items" data-list="items" + <template> child Template goes inside the list element
:class="expr" data-bind-class="expr" Dynamic class binding
:style="expr" data-bind-style="expr" Dynamic style binding
x-init init() lifecycle hook Defined in component JS definition
x-effect Computed properties or init() Derived values use computed; side effects use init()
Alpine.store() wildflower.store() Very similar API
$store.name $name.prop or subscribe Cross-entity access in HTML, or subscribe pattern in JS

Side-by-Side: Todo List

The best way to see the differences is a complete example. Here's the same todo app in both frameworks:

Alpine.js

<div x-data="{
    todos: [],
    newTodo: '',
    get 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(index) {
        this.todos.splice(index, 1);
    }
}">
    <h3>Todos (<span x-text="remaining"></span> remaining)</h3>
    <input x-model="newTodo" @keydown.enter="addTodo()" />
    <button @click="addTodo()">Add</button>
    <ul>
        <template x-for="(todo, index) in todos" :key="todo.id">
            <li>
                <input type="checkbox" x-model="todo.done" />
                <span x-text="todo.text"
                      :style="{ textDecoration: todo.done
                          ? 'line-through' : 'none' }"></span>
                <button @click="removeTodo(index)">&times;</button>
            </li>
        </template>
    </ul>
</div>

Everything — state, getters, methods — lives inline in the HTML attribute.

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">&times;</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);
    }
});

HTML is clean markup. Logic lives in a separate JS definition.

Key differences to notice:
  • Alpine puts state inline in x-data; WildflowerJS uses a named data-component with JS definition
  • Alpine's get remaining() re-evaluates on every access; WildflowerJS's computed caches the result
  • Alpine's x-for uses (todo, index) in todos; WildflowerJS's data-list gets index from the action details parameter
  • The <template> in WildflowerJS goes inside the data-list element, not as a sibling

Pattern-by-Pattern Migration

Component Definition

Alpine inlines everything in x-data. WildflowerJS separates markup from behavior.

Alpine
<div x-data="{
    count: 0,
    get doubled() { return this.count * 2; },
    increment() { this.count++; }
}">
    <span x-text="doubled"></span>
    <button @click="increment()">+</button>
</div>
WildflowerJS
<div data-component="counter">
    <span data-bind="doubled"></span>
    <button data-action="increment">+</button>
</div>
wildflower.component('counter', {
    state: { count: 0 },
    computed: {
        doubled() { return this.count * 2; }
    },
    increment() { this.count++; }
});

State

Alpine's state is an inline object. WildflowerJS uses a state property in the component definition.

Alpine
<div x-data="{ name: '', email: '', agreed: false }">
    ...
</div>
WildflowerJS
wildflower.component('signup-form', {
    state: {
        name: '',
        email: '',
        agreed: false
    }
});
<div data-component="signup-form">...</div>

Computed / Getters

Alpine uses JavaScript getter syntax that re-evaluates on every access. WildflowerJS computed properties are cached and only recalculate when their dependencies change.

Alpine
<div x-data="{
    items: [],
    get total() {
        // Runs EVERY time 'total' is read
        return this.items.reduce((s, i) => s + i.price, 0);
    },
    get expensive() {
        return this.items.filter(i => i.price > 100);
    }
}">
    <span x-text="total"></span>
</div>
WildflowerJS
wildflower.component('cart', {
    state: { items: [] },
    computed: {
        // Cached! Only re-runs when items changes
        total() {
            return this.items.reduce((s, i) => s + i.price, 0);
        },
        expensive() {
            return this.items.filter(i => i.price > 100);
        }
    }
});
Performance win: If your Alpine component has expensive getters that get accessed multiple times per render cycle, WildflowerJS computed properties will only calculate once and cache the result until dependencies change.

Event Handling

Alpine allows inline JavaScript expressions in event handlers. WildflowerJS uses named methods.

Alpine
<!-- Inline expression -->
<button @click="count++">+</button>

<!-- Method call -->
<button @click="handleSubmit()">Submit</button>

<!-- With modifiers -->
<form @submit.prevent="save()">
<button @click.stop="close()">X</button>

<!-- Key modifiers -->
<input @keydown.enter="search()" />
<input @keydown.escape="clear()" />
WildflowerJS
<!-- Named method -->
<button data-action="increment">+</button>

<!-- Same pattern -->
<button data-action="handleSubmit">Submit</button>

<!-- Modifiers in the method -->
<form data-action="submit:save">
<button data-action="close">X</button>

<!-- Key modifiers -->
<input data-action="keydown.enter:search" />
<input data-action="keydown.escape:clear" />
// Handle event modifiers in JS
save(event) {
    event.preventDefault();
    // save logic...
},
close(event) {
    event.stopPropagation();
    // close logic...
}

Conditional Rendering

Both frameworks have show/hide and insert/remove patterns. The syntax is nearly identical.

Alpine
<!-- CSS toggle (stays in DOM) -->
<div x-show="isOpen">Dropdown content</div>

<!-- DOM insertion/removal -->
<template x-if="isLoggedIn">
    <div>Welcome back!</div>
</template>
WildflowerJS
<!-- CSS toggle (stays in DOM) -->
<div data-show="isOpen">Dropdown content</div>

<!-- DOM insertion/removal -->
<div data-render="isLoggedIn">Welcome back!</div>
Note: Alpine's x-if requires a wrapping <template> element. WildflowerJS's data-render goes directly on the element you want to conditionally render. No wrapper needed.

List Rendering

Both use <template> elements, but the structure differs.

Alpine
<ul>
    <template x-for="item in items" :key="item.id">
        <li>
            <span x-text="item.name"></span>
            <span x-text="item.price"></span>
        </li>
    </template>
</ul>

Template is a sibling/child of the list container. Key is on the template.

WildflowerJS
<ul data-list="items" data-key="id">
    <template>
        <li>
            <span data-bind="name"></span>
            <span data-bind="price"></span>
        </li>
    </template>
</ul>

Template goes inside the data-list element. Key is on the list container. No item. prefix needed in bindings.

Keyed reconciliation: WildflowerJS's data-key enables efficient DOM reuse when items are reordered, inserted, or removed, similar to React's key prop but with true keyed reconciliation instead of Alpine's full re-render.

Forms

Two-way binding is nearly identical between the frameworks.

Alpine
<input type="text" x-model="username" />
<textarea x-model="bio"></textarea>
<select x-model="role">
    <option value="admin">Admin</option>
    <option value="user">User</option>
</select>
<input type="checkbox" x-model="agreed" />
WildflowerJS
<input type="text" data-model="username" />
<textarea data-model="bio"></textarea>
<select data-model="role">
    <option value="admin">Admin</option>
    <option value="user">User</option>
</select>
<input type="checkbox" data-model="agreed" />

Stores

Both frameworks have a global store concept, but WildflowerJS adds declarative subscriptions with automatic cleanup.

Alpine
Alpine.store('cart', {
    items: [],
    get total() {
        return this.items.reduce((s, i) => s + i.price, 0);
    },
    add(item) {
        this.items.push(item);
    }
});
<!-- Access in any component -->
<span x-text="$store.cart.total"></span>
<button @click="$store.cart.add(item)">Add</button>
WildflowerJS
wildflower.store('cart', {
    state: { items: [] },
    computed: {
        total() {
            return this.items.reduce((s, i) => s + i.price, 0);
        }
    },
    add(item) {
        this.items.push(item);
    }
});
<!-- Access in any component -->
<span data-bind="$cart.total"></span>
// Or subscribe in a component definition
wildflower.component('checkout', {
    subscribe: { cart: ['items'] },
    onStoreUpdate(store, path, newVal) {
        // React to store changes
    }
});

Scaling Up

Alpine is designed for "sprinkling" interactivity. WildflowerJS has the architecture to scale from sprinkles to full SPAs.

Alpine (typical usage)
<!-- Individual interactive widgets -->
<div x-data="{ open: false }">
    <button @click="open = !open">Menu</button>
    <nav x-show="open">...</nav>
</div>

<div x-data="searchWidget()">...</div>
<div x-data="cartWidget()">...</div>

<!-- No routing, no nesting, no SSR -->
WildflowerJS (full SPA)
<!-- App shell with routing -->
<div data-component="app-shell">
    <nav data-component="main-nav">...</nav>
    <main data-route="/dashboard">
        <div data-component="dashboard">
            <div data-component="stats-panel">...</div>
            <div data-component="activity-feed">...</div>
        </div>
    </main>
</div>

<!-- Nested components, routing, stores, SSR -->

Common Gotchas

No Inline State

State and logic live in a JavaScript file, not in HTML attributes. There is no equivalent to x-data="{ count: 0 }" with inline state. Always define a named component with wildflower.component('name', { state: {...} }).

No Inline Expressions in Actions

Alpine lets you write @click="count++". WildflowerJS requires named methods:

<!-- Alpine -->
<button @click="count++">+</button>
<button @click="items.push({id: Date.now()})">Add</button>

<!-- WildflowerJS -->
<button data-action="increment">+</button>
<button data-action="addItem">Add</button>
Computed Properties Are Cached

Unlike Alpine getters that re-run every time they are accessed, WildflowerJS computed properties only recalculate when their dependencies change. This is a feature, not a bug. It means expensive computations like filtering or reducing large arrays happen much less often.

List Template Is a Child, Not a Sibling

The <template> element goes inside the data-list element:

<!-- Alpine: template is a direct child of the container -->
<ul>
    <template x-for="item in items"><li x-text="item.name"></li></template>
</ul>

<!-- WildflowerJS: data-list goes on the container, template inside -->
<ul data-list="items">
    <template><li data-bind="name"></li></template>
</ul>
No $refs or $dispatch

Alpine provides magic properties like $el, $refs, and $dispatch. WildflowerJS has its own this.$el(selector), a jQuery-like DOM helper for querying within the component (see DOM Helpers). There are no $refs or $dispatch equivalents. Instead, action methods receive the event and element as parameters: myMethod(event, element, details). For cross-component communication, use stores with subscriptions or the $name.prop cross-entity access syntax in HTML.

Component Registration Is Required

You must call wildflower.component('name', {...}) before using data-component="name" in HTML. Alpine's x-data works inline without any prior registration. Think of WildflowerJS components like Vue or React components. They must be defined before they are used.

Scaling Beyond Alpine

If you chose Alpine because you wanted something lightweight and buildless, WildflowerJS gives you the same starting point with room to grow. Here are the capabilities you gain:

Component Lifecycle

Alpine has x-init but no destroy hook. WildflowerJS provides proper lifecycle management:

wildflower.component('live-feed', {
    state: { messages: [] },
    init() {
        // Runs when component mounts
        this._ws = new WebSocket('wss://api.example.com/feed');
        this._ws.onmessage = (e) => {
            this.messages.push(JSON.parse(e.data));
        };
    },
    destroy() {
        // Runs when component unmounts: no memory leaks
        this._ws.close();
    }
});

Store Subscriptions

Alpine stores are global reactive objects. WildflowerJS stores add declarative subscriptions with automatic cleanup:

wildflower.component('notification-badge', {
    state: { unreadCount: 0 },
    subscribe: {
        notifications: ['items']  // Watch notifications store
    },
    onStoreUpdate(storeName, path, newValue) {
        if (storeName === 'notifications') {
            this.unreadCount = newValue.filter(n => !n.read).length;
        }
    }
    // Subscription is automatically cleaned up on component destroy
});

SPA Routing

Alpine has no built-in routing. WildflowerJS includes a full RouteManager:

<nav data-component="main-nav">
    <a href="/dashboard">Dashboard</a>
    <a href="/settings">Settings</a>
</nav>

<main>
    <div data-route="/dashboard" data-component="dashboard">...</div>
    <div data-route="/settings" data-component="settings">...</div>
</main>

Nested Components and Communication

Alpine components are isolated islands. WildflowerJS supports nested components with props and slots:

<div data-component="user-profile">
    <div data-component="avatar-card" data-props="{ name: userName, image: userAvatar }">
        <img data-bind-attr="{ src: props.image }" />
        <span data-bind="props.name"></span>
    </div>
    <div data-component="activity-timeline" data-props="{ userId: id }">
        ...
    </div>
</div>

Server-Side Rendering

WildflowerJS includes an SSR manager for rendering components on the server with hydration on the client, something Alpine was never designed for.

Migration Checklist

Follow these steps to migrate an Alpine.js project to WildflowerJS:

  1. Extract inline x-data objects into wildflower.component() definitions in a separate JS file
  2. Replace x-* attributes with data-* equivalents using the attribute mapping table above
  3. Move inline event handlers (@click="count++") to named methods in the component definition
  4. Convert Alpine.store() calls to wildflower.store() with explicit state and computed blocks
  5. Add lifecycle hooks where needed: replace x-init with init(), add destroy() for cleanup
  6. Add data-key to list containers for efficient keyed reconciliation
  7. Restructure as nested components for larger pages: break monolithic x-data blocks into composable components
  8. Test each component after migration: verify state, events, computed values, and list rendering all work correctly