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.

Entity Pools LITE+

A rendering primitive for collections of reactive entities. Each pool holds many entities of the same shape; data-pool keeps their DOM in sync with a single batch flush per frame. Entities use the same unified entity model as components, stores, and plugins.

Pool entities are entities. They declare state, computed, and methods the same way components, stores, and plugins do. See Defining an entity below. What makes pools different is multiplicity, reactivity mode, and timing: a pool holds many entities of one shape (keyed by id), entity state is plain JS rather than Proxy-tracked, and the pool syncs the DOM in one batch each animation frame instead of on every property mutation.
When to Use Pools: Start with data-list. It's the right default. Reach for data-pool when you need more performance, explicit control over what updates, or high-frequency rendering (real-time data, animation, games). For items that need two-way binding (data-model) or nested lists, use data-list.

New to pools? Read Why Pools? to understand why WildflowerJS offers two reactivity modes and how they compare.

The Unified Entity Model

A WildflowerJS entity — whether it's a component, a store, a plugin, or a pool — declares the same four things: state, computed, methods, and lifecycle hooks. The shape is identical; only the scope and binding differ. (For pools, the four things live inside the pool's entity block, since one pool holds many items.)

Component
wildflower.component('counter', {
    state: { count: 0 },
    computed: {
        doubled() { return this.count * 2; }
    },
    increment() { this.count++; },
    init() { /* lifecycle */ }
})
Store
wildflower.store('cart', {
    state: { items: [] },
    computed: {
        total() { return this.items.length; }
    },
    add(item) { this.items.push(item); },
    init() { /* lifecycle */ }
})
Plugin
wildflower.plugin('timer', {
    state: { elapsed: 0 },
    computed: {
        seconds() { return this.elapsed / 1000; }
    },
    reset() { this.elapsed = 0; },
    init() { /* lifecycle */ }
})
Pool Entity
pools: {
    enemies: {
        entity: {
            state: { hp: 100 },
            computed: {
                isDead() { return this.hp <= 0; }
            },
            takeDamage(n) { this.hp -= n; }
        }
    }
}

What distinguishes pools from the other three:

  • Multiplicity. A component, store, or plugin is declared once and has one instance (or one instance per mounted element). A pool holds many items of the declared entity shape — spawn, key, render, remove — with one entity block defining all of them. Those items are also called pool entities, and they share the pool's single reactive context rather than each getting one of their own.
  • Reactivity mode. Component, store, and plugin state is wrapped in a reactive Proxy. Every property mutation is observed, and dependent computed properties and bindings update immediately. Pool entity state is plain JavaScript: mutations aren't tracked. The pool re-reads entity values on each flush and writes only what changed. This is why pools scale to thousands of entities without per-property overhead.
  • Timing. Push vs pull. Component bindings update synchronously when state changes. Pool bindings update in a single batched DOM write per animation frame.

Two Kinds of Reactivity

WildflowerJS offers two reactive rendering modes. Both are declarative; you describe what the DOM should look like, and the framework keeps it in sync. The difference is timing:

data-list vs data-pool comparison
  • data-list (push-based): the framework watches for property changes via Proxy and updates the DOM immediately. Best for correctness-critical UI where every state change should reflect instantly.
  • data-pool (pull-based): the framework reads the current state of all entities each animation frame and syncs the DOM in a single batch. Best for throughput-critical scenarios where many entities change simultaneously.

Both use the same template bindings (data-bind, data-bind-style, data-bind-class, data-bind-attr, data-show). Both manage entity lifecycles automatically. The developer picks the right mode for the job.

data-list data-pool
Reactivity Push-based (Proxy per property) Pull-based (rAF batch flush)
Items Reactive proxy objects Plain JS objects
Updates Immediate on mutation Batched per animation frame
Template scope Full component context (state, computed, stores) Entity properties + shared props
Two-way binding Yes (data-model) No
Actions Yes (data-action) Yes (data-action via event delegation)
Use case Interactive collections (forms, inline editing, two-way binding) Performance-sensitive CRUD, real-time data, large datasets, per-frame animation

Basic Usage

A DOM-rendered pool needs three things: a container element with data-pool, a data-key attribute identifying the unique property, and a <template> child defining each entity's DOM structure. Pools declared in a pools: {} block without a matching DOM element work as headless data collections, useful in stores or components that feed data to other parts of the UI.

HTML Template

<div data-component="particle-demo">
    <button data-action="spawn">Spawn Particle</button>

    <div class="world" data-pool="particles" data-key="id">
        <template>
            <div class="particle"
                 data-bind-style="{ left: x + 'px', top: y + 'px', opacity: alpha }">
            </div>
        </template>
    </div>
</div>

JavaScript

wildflower.component('particle-demo', {
    state: { nextId: 1 },

    // Declare pools alongside state: the framework sets them up automatically
    pools: { particles: {} },

    spawn() {
        this.pools.particles.add({
            id: this.nextId++,
            x: Math.random() * 400,
            y: Math.random() * 300,
            alpha: 1,
            vy: -2 - Math.random() * 3
        });
    },

    // tick(dt) is called automatically each animation frame.
    // dt = milliseconds since last frame. Physics runs here; pool flush happens right after.
    tick(dt) {
        var t = dt / 16.67; // normalize: t≈1.0 at 60fps
        for (const p of this.pools.particles.items) {
            p.y += p.vy * t;
            p.alpha -= 0.01 * t;
        }
        // Remove dead particles
        for (let i = this.pools.particles.items.length - 1; i >= 0; i--) {
            if (this.pools.particles.items[i].alpha <= 0) {
                this.pools.particles.remove(this.pools.particles.items[i].id);
            }
        }
    }
});
Template scope: Pool templates can reference entity properties and shared props.* values (see Pool Props). Component state, computed properties, and store values are not directly available. Use pool.props to pass shared data into the template. This keeps items free of per-item Proxy overhead.

Defining an Entity

The minimal pool above treats entities as plain objects, useful for pure data passthrough. For richer pools, the entity block inside a pool definition declares the entity's shape: state, computed, and methods. This is the same shape components, stores, and plugins use.

pools: {
    enemies: {
        entity: {
            // Default state merged into every new entity at add() time.
            // Spawn-provided values always win over these defaults.
            state: {
                hp: 100,
                maxHp: 100,
                eState: 'follow'
            },

            // Per-entity derived values. Recomputed on every read (no cache).
            // `this` is the entity.
            computed: {
                isDead()   { return this.hp <= 0; },
                hpPercent(){ return Math.round((this.hp / this.maxHp) * 100); }
            },

            // Methods at the top level of the entity block. `this` is the entity.
            // Auto-routed when data-action names them (falls back to the
            // component method if the entity doesn't define one).
            takeDamage(n) {
                this.hp -= n;
                if (this.hp <= 0) this.eState = 'dying';
            }
        }
    }
}

entity.state: default values merged into new entities

Fields declared in entity.state are merged into every new entity at add() / push() time. The merge is shallow: spawn-provided values win, and any field the spawn omits is filled in from the template.

// With the enemies pool above:
pool.push({ id: 1 });
// Entity becomes: { id: 1, hp: 100, maxHp: 100, eState: 'follow' }

pool.push({ id: 2, hp: 250 });
// Entity becomes: { id: 2, hp: 250, maxHp: 100, eState: 'follow' }

entity.computed: per-entity derived values

Functions declared in entity.computed become getters on each entity. They recompute on every read, with this bound to the entity. Template bindings (data-bind, data-bind-class, data-show, etc.) read computed names the same way they read plain property names.

<div data-pool="enemies" data-key="id">
    <template>
        <div data-bind-class="isDead ? 'enemy dying' : 'enemy'">
            HP: <span data-bind="hpPercent"></span>%
        </div>
    </template>
</div>

Computed properties recompute on every access; there is no cache. Entity mutations (including those from entity methods) are visible to the next binding read with no invalidation step.

Performance note. Each computed is implemented as a property getter, which costs roughly 60 μs per entity per flush (a function call plus whatever the expression does). For event-driven pools (todo lists, dashboards, turn-based state, anything that flushes tens to low-hundreds of entities on user actions) the cost is invisible and the declarative win is worth it.

For hot animation loops (hundreds of entities, 60 fps), that cost adds up fast: 800 entities × 60 fps × 60 μs is about 3 ms/frame. In those cases, prefer a plain data property that you assign at mutation time (e.g. entity.cssClass = 'enemy dying' when you set eState = 'dying'). Use entity.computed when ergonomics matter more than microseconds; use data properties when microseconds matter more than ergonomics.

Entity methods: behaviors scoped to a single entity

Any function property on the entity block that isn't state or computed is an entity method. Methods are installed on every entity at add() time; this is the entity itself.

// Call directly on any entity
pool.get(1).takeDamage(25);

// Or via data-action inside the template, automatically routed to the
// entity method, with `this` bound to the clicked entity.
<button data-action="takeDamage">Hit</button>

When both the component and an entity define a method with the same name, the entity's wins. Methods not defined on the entity fall back to component methods.

Arrow functions will throw. Entity methods need this bound to the entity at call time, so arrow functions are rejected at pool registration with a clear error. Use shorthand method syntax: takeDamage(n) { ... }, not takeDamage: (n) => { ... }.

Entity scope: what this can and can't reach

Inside an entity.computed getter or an entity method, this is the entity itself. The framework doesn't plumb additional context into that call site. Everything else you might use is ordinary JavaScript scope, not a framework accessor.

Reachable from an entity function:

  • this: the entity's own properties (fields set at spawn, defaults from entity.state, installed computed, other entity methods)
  • Module-level variables and helper functions via normal lexical closure (constants, imported utilities, precomputed tables)
  • The wildflower global, which exposes framework-level APIs such as wildflower.getStore('name'). Any function in your code can call it; there's nothing special about entity methods here.

Not reachable from an entity function:

  • The pool handle. The entity doesn't carry a back-reference to its pool, so there's no way to call pool.remove(this.id) from an entity method. Removal lives on the component.
  • Pool-level props. Template bindings can read props.X, but the pool doesn't inject a $props or similar accessor into entity functions.
  • The parent component's state, computed, methods, or other pools. The entity has no this.parent, no inherited this.pools, nothing that reaches upward.

This isn't a hard sandbox. A determined author can hold references to the pool or component in module-level variables and reach back in. The framework simply doesn't make such access part of the entity surface, which shapes a natural split: behavior that fits inside the entity goes in the entity; coordination that spans the pool or the rest of the app lives on the owning component. Decide by what the code is actually doing, not by what's syntactically possible.

// Common pattern: entity-local mutation as entity method,
// pool-level coordination as a component method that entity actions
// fall through to when the entity doesn't define a match.

pools: {
    enemies: {
        entity: {
            state: { hp: 100 },
            takeDamage(n) {
                this.hp -= n;
                // Scoring is global, so reach the store directly.
                wildflower.getStore('stats').damageDealt += n;
            }
        }
    }
},

// Component method. Handles the things an entity method can't express
// cleanly. In this case, removing the entity from its pool.
removeEnemy(item) {
    this.pools.enemies.remove(item.id);
}

Pool API (array-like)

Pool handles expose a selected array-like surface: push, length, at(i), plus the read-only helpers find, filter, map, forEach, some, every, and reduce. Pools are also iterable. Entities are addressed by key, not by index, so methods like splice, pop, indexOf, and slice aren't exposed; they require stable index positions, which swap-with-last storage doesn't provide.

pool.push({ id: 1, hp: 10 });
pool.length;                     // 1
for (const enemy of pool) { ... }
pool.find(e => e.isDead);
pool.get(1).takeDamage(5);
pool.remove(1);
pool.at(0);                      // entity at DOM position 0 (stable)

add, remove, and size are aliases for push, remove by key, and length. Both spellings have identical behavior and performance. Use whichever reads better in your code.

Template Bindings

Pool templates support these binding attributes, all operating on entity object properties:

Attribute Purpose Example
data-bind Text content <span data-bind="label"></span>
data-bind-style Inline styles (object expression) data-bind-style="{ left: x + 'px', top: y + 'px' }"
data-bind-class CSS classes (string, array, or object) data-bind-class="hp > 0 ? 'alive' : 'dead'"
data-bind-attr HTML attributes data-bind-attr="{ src: imgSrc, width: w, height: h }"
data-show Visibility toggle data-show="visible"

Expressions work the same as in regular bindings; they reference entity properties as variables:

<template>
    <div data-bind-style="{
             left: x + 'px',
             top: y + 'px',
             transform: 'rotate(' + rotation + 'deg) scaleX(' + (facingLeft ? -1 : 1) + ')'
         }"
         data-bind-class="state === 'dying' ? 'entity dying' : 'entity'">
        <img data-bind-attr="{ src: imgSrc, width: w, height: h }" draggable="false">
        <div class="hp-bar" data-show="showHp">
            <div class="hp-fill" data-bind-style="{ width: (hp / maxHp * 100) + '%' }"></div>
        </div>
        <span class="label" data-bind="label"></span>
    </div>
</template>
Next: For lifecycle hooks, animation with tick(), event handling, static pools, bulk operations, and pool props, see Advanced Pools. For the full API reference, see Pool API.