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.

Advanced Pools LITE+

Lifecycle hooks, animation, event handling, static pools, bulk operations, and shared props: the features that make pools a complete rendering primitive.

Pool Lifecycle Hooks

Pools support lifecycle hooks for managing external resources (physics bodies, audio sources, network connections) tied to entity lifetime:

wildflower.component('physics-demo', {
    pools: {
        balls: {
            onAdd: 'onBallAdd',       // called after entity added
            onRemove: 'onBallRemove', // called before individual removal
            onClear: 'onBallsClear'   // called once on bulk clear (skips onRemove)
        }
    },

    onBallAdd(item) {
        // Create a physics body and attach it to the entity
        item._body = PhysicsEngine.createBody(item.x, item.y, item.radius);
    },

    onBallRemove(item) {
        // Clean up physics body + play sound effect
        PhysicsEngine.destroyBody(item._body);
        SFX.play('clink');
    },

    onBallsClear(items) {
        // Efficient bulk teardown: no per-item sounds
        PhysicsEngine.clearAll();
    }
})
Hook resolution: Hooks can be string references to component methods (preferred) or inline functions. String references are resolved the same way as data-action: the framework looks up the method on the component and binds this automatically.

onClear fallback: If onClear is not defined, pool.clear() fires onRemove for each item individually. If onClear IS defined, onRemove is skipped during clear, leaving bulk cleanup to the developer.

The tick(dt) Lifecycle Hook

Components that define a tick(dt) method get it called automatically once per animation frame. The framework manages the requestAnimationFrame loop. You never need to set one up or tear one down.

wildflower.component('boids', {
    pools: { boids: {} },

    init() {
        // ... spawn entities ...
    },

    tick(dt) {
        // dt = milliseconds since last frame (clamped to 250ms)
        var t = dt / 16.67; // normalize: t≈1.0 at 60fps
        for (const b of this.pools.boids.items) {
            b.x += b.vx * t;
            b.y += b.vy * t;
        }
        // Pool flush happens automatically right after tick returns
    }
    // No destroy() needed: framework cleans up the rAF loop
});

tick(dt) also receives a second argument now (performance.now() timestamp) for components that need absolute time.

Execution order: On each animation frame, the framework calls tick(dt) on all tickable components FIRST, then flushes all pool bindings to the DOM. This means entity mutations made in tick are visible in the DOM on the same frame.

tick(dt) works with or without pools. A component with tick but no data-pool still gets the rAF loop, useful for canvas animations, Three.js scenes, or any per-frame logic.

Tip: For HUD elements outside the pool template (FPS counters, score displays), use direct DOM writes like document.getElementById('fps').textContent = fps. This avoids reactive overhead for values that update every frame. See Pool Performance for details.

Event Handling

Pool templates support data-action inside templates. Actions are routed to entity methods first (with this bound to the clicked entity) and fall back to component methods if no entity method matches the name.

Prefer entity methods when the handler only mutates the entity itself: no id lookup, no parent-state plumbing:

<div data-pool="tasks" data-key="id">
    <template>
        <div class="task">
            <span data-bind="title"></span>
            <button data-action="toggleDone">Done</button>
            <button data-action="removeTask">Delete</button>
        </div>
    </template>
</div>
wildflower.component('tasks-demo', {
    pools: {
        tasks: {
            entity: {
                state: { done: false },
                // Entity method: `this` is the task
                toggleDone() { this.done = !this.done; }
            }
        }
    },

    // Component method: entity methods can't remove themselves from the pool,
    // so removal lives at the component level and falls back via data-action.
    removeTask(item) { this.pools.tasks.remove(item.id); }
});

For multi-target event delegation (different actions for different sub-elements of a template), data-pool-action on the container accepts selector-specific bindings. See Pool API for the full format reference.

Real-World Example: Game Entities

Here is a pattern from the WildflowerJS Tower Defense demo, showing multiple pools working together: enemies, projectiles, towers, and loot drops, all rendering at native frame rate. Each entity declares its shape via an entity block (see Defining an Entity) so the spawn code stays focused on the instance-specific values.

<div data-component="tower-defense">
    <div class="game-field">
        <!-- Each pool manages its own entity type -->
        <div data-pool="towers" data-key="index">
            <template>
                <div data-bind-class="towerClass"
                     data-bind-style="{ left: x + 'px', top: y + 'px' }">
                    <img data-bind-attr="{ src: imgSrc }" width="48" height="48">
                </div>
            </template>
        </div>

        <div data-pool="enemies" data-key="id">
            <template>
                <!-- className derived by the entity.computed below -->
                <div data-bind-class="className"
                     data-bind-style="{ left: x + 'px', top: y + 'px',
                                        width: w + 'px', height: h + 'px' }">
                    <img data-bind-attr="{ src: imgSrc, width: w, height: h }">
                </div>
            </template>
        </div>

        <div data-pool="projectiles" data-key="id">
            <template>
                <div class="projectile"
                     data-bind-style="{ left: x + 'px', top: y + 'px' }">
                    <img data-bind-attr="{ src: imgSrc }">
                </div>
            </template>
        </div>

        <div data-pool="loots" data-key="id">
            <template>
                <div class="loot" data-bind-style="{ left: x + 'px', top: y + 'px' }">
                    <img data-bind-attr="{ src: imgSrc }" width="14" height="14">
                </div>
            </template>
        </div>
    </div>
</div>
wildflower.component('tower-defense', {
    state: { nextId: 1 },

    pools: {
        enemies: {
            entity: {
                // Defaults shared across every enemy
                state: { speedX: 0, speedY: 1, eState: 'follow', hpPct: 100 },

                // className derived from state, flips to "enemy dying"
                // when takeDamage() sets eState. No imperative class stitching.
                computed: {
                    className() { return this.eState === 'dying' ? 'enemy dying' : 'enemy'; }
                },

                // `this` is the enemy entity
                takeDamage(n) {
                    this.hp -= n;
                    if (this.hp <= 0) this.eState = 'dying';
                }
            }
        },
        projectiles: {},
        loots: {},
        towers: {}
    },

    spawnEnemy(type, x, y) {
        // Only instance-specific fields. Defaults and derivations are declared above.
        this.pools.enemies.push({
            id: this.nextId++,
            x, y,
            hp: type.maxHp,
            maxHp: type.maxHp,
            imgSrc: type.sprite,
            w: type.width,
            h: type.height
        });
    },

    // Called automatically each frame: no rAF setup needed
    tick(dt) {
        var t = dt / 16.67; // normalize for frame-rate independence

        // Update all enemies: direct property mutation
        for (const e of this.pools.enemies) {
            e.y += e.speedY * t;
        }

        // Update all projectiles
        for (const p of this.pools.projectiles) {
            p.x += p.vx * t;
            p.y += p.vy * t;
        }

        // Remove dead enemies (after their dying animation finishes)
        this.pools.enemies.forEach(e => {
            if (e.eState === 'dying' && performance.now() - e._diedAt > 600) {
                this.pools.enemies.remove(e.id);
            }
        });
    }
});
See it in action: The Tower Defense demo renders 100+ animated entities at native frame rate using four simultaneous pools: enemies, towers, projectiles, and loot drops.

Static Pools

For collections that don't need per-frame updates, add the data-pool-static boolean attribute. This skips the rAF flush loop entirely. Items are rendered synchronously on add() and update(), with zero idle CPU cost.

<!-- Static pool: renders on add/update only, no rAF loop -->
<tbody data-pool="users" data-key="id" data-pool-static>
    <template>
        <tr>
            <td data-bind="name"></td>
            <td data-bind="status"
                data-bind-class="status === 'active' ? 'badge active' : 'badge inactive'"></td>
        </tr>
    </template>
</tbody>
// Populate: items render immediately (bulk add via array)
this.pools.users.add(data);

// Update: DOM reflects the change synchronously
this.pools.users.update('user-42', { status: 'active' });

// Direct property mutation does NOT update DOM in static pools.
// Always use pool.update() to apply changes.
Note: data-pool-static with no value makes the entire pool static (passive). data-pool-static="propName" with a value is a different feature: it marks individual entities as static within a live pool based on an entity property.

Bulk Add

Pass an array to pool.add() to insert multiple items in a single DOM operation via DocumentFragment. This is significantly faster than adding items one at a time for large collections.

// Single add (object)
this.pools.items.add({ id: 1, name: 'Alice' });

// Bulk add (array): single DOM operation
this.pools.items.add([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
    { id: 3, name: 'Carol' }
]);

Bulk add works with both live and static pools, fires onAdd lifecycle hooks for each item, and skips items with duplicate keys.

Pool Props

Pool props are shared data injected by the parent component, available to all pool item expressions via the props. prefix. Props are a plain object. They can hold scalars, arrays, objects, or any structure. Each item resolves props.* at render time, so a single props update can mean different things to different items.

// Declare props: scalars, objects, anything
pools: {
    users: {
        props: { showLabels: true, currency: '$' }
    }
}
<!-- Reference props in template expressions -->
<div data-pool="users" data-key="id">
    <template>
        <div>
            <span data-bind="name" data-show="props.showLabels"></span>
            <span data-bind="props.currency + price"></span>
        </div>
    </template>
</div>
// Update props: one write, every item picks it up on the next flush
this.pools.users.props.currency = '€';

Props aren't limited to scalars. They can hold objects, arrays, or any structure, and items can index into them using their own properties. This lets a single prop mean different things to different items:

// Per-flock shape lookup: each item resolves its own shape
pool.props.shape = { 0: '', 1: 'shape-circle', 2: 'shape-diamond' };

// In the template: props.shape[flock] resolves per item
Props vs item properties: Props are shared (one object for the entire pool, zero per-item overhead). Item properties are unique per entity. Use props for anything multiple items need access to, whether it applies uniformly or is resolved per-item through a lookup.

See it in action: The Boids demo uses props.shape as a per-flock lookup table. Select "Mixed" from the Shape dropdown to see 800 boids render with different shapes based on their flock color.

Things to Know

Item Order After Removal

Pools use O(1) swap-with-last removal for performance. When you call pool.remove(key), the removed item is swapped with the last item in the pool.items array before being removed. This means pool.items order is not guaranteed after removals.

The DOM order is unaffected. Elements stay in their insertion positions. But if you iterate pool.items and expect insertion order after removals, use pool.get(key) to access items by key instead of by array index.

If your use case requires guaranteed ordering, use data-list instead.

Template Scope

Pool templates can reference entity properties and props.* values. Unlike data-list, pool templates cannot access parent component state, stores, or computed properties directly. Use pool.props to bridge shared data into the template. This is the tradeoff that eliminates reactive proxy overhead.

Continue Learning: For the full API reference including throttling, culling, and sorting options, see Pool API. For performance optimization techniques, see Pool Performance.