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.

Pool API LITE+

Complete reference for entity pool attributes, methods, and configuration options.

Prerequisites: This page covers the full pool API. For background, see Why Pools?. For an introduction to entity pools, see Entity Pools.

HTML Attributes

data-pool

Declares an element as a pool container. The attribute value is the pool name, used to access the pool via this.pool(name) in component methods.

<div data-pool="enemies" data-key="id">
    <template>
        <!-- Entity template -->
    </template>
</div>

The container element must include:

  • A data-key attribute specifying which entity property is the unique identifier (defaults to "id" if omitted)
  • A <template> child element defining the DOM structure for each entity

data-key

Specifies the entity property used as a unique identifier. Every entity added to the pool must have this property. Defaults to "id".

<!-- Uses 'id' property (default) -->
<div data-pool="enemies" data-key="id">...</div>

<!-- Uses 'index' property as key -->
<div data-pool="towers" data-key="index">...</div>

<!-- Uses 'uuid' property as key -->
<div data-pool="cursors" data-key="uuid">...</div>
Duplicate keys are rejected. If you call add() with an object whose key already exists in the pool, the add is silently ignored (with a console warning in dev builds). Remove the existing entity first if you need to replace it.

data-pool-fps

Throttles the pool's flush rate to a target frames-per-second. When set, the pool skips rAF frames until enough time has elapsed since the last flush. Useful for reducing CPU usage on pools that do not need 60fps updates.

<!-- Update this pool at most 30 times per second -->
<div data-pool="dashboard-items" data-key="id" data-pool-fps="30">
    <template>
        <div class="metric">
            <span data-bind="label"></span>:
            <span data-bind="value"></span>
        </div>
    </template>
</div>
ValueBehavior
0 or omittedFlush every rAF frame (~60fps on most displays)
30Flush at most 30 times per second (~33ms between flushes)
10Flush at most 10 times per second (~100ms between flushes)

data-pool-cull

Enables spatial culling with the specified padding in pixels. When enabled, the pool checks each entity's bounding rectangle against the visible viewport on every flush. Entities outside the viewport (plus padding) have their visibility set to hidden, which allows the browser to skip paint and compositing for those elements.

<!-- Cull entities more than 100px outside the viewport -->
<div data-pool="world-objects" data-key="id" data-pool-cull="100">
    <template>
        <div class="object" data-bind-style="{ left: x + 'px', top: y + 'px' }">
            <img data-bind-attr="{ src: sprite }">
        </div>
    </template>
</div>

The padding value adds a buffer zone around the viewport. Entities within this buffer are kept visible so they do not pop in as the user scrolls or the camera pans:

  • data-pool-cull="0": cull exactly at the viewport edge
  • data-pool-cull="100": keep entities visible within 100px outside the viewport
  • data-pool-cull="200": larger buffer for fast-scrolling scenarios
  • Omitted: no culling (all entities always rendered)

The cull boundary is computed from the pool container's parent element (clamped to the actual viewport). This handles scrollable or pannable game worlds where the container is larger than the screen.

data-pool-cull-props

Enables data-based culling, reading entity position (and optionally size) from data properties instead of calling getBoundingClientRect() per entity per frame. This eliminates layout-forcing DOM reads entirely and enables the pool to skip both culling math AND binding evaluation for offscreen entities.

<!-- Read position from x,y properties, use template-measured default size -->
<div data-pool="sprites" data-key="id" data-pool-cull="100" data-pool-cull-props="x,y">
    <template>...</template>
</div>

<!-- Read position AND size from entity properties -->
<div data-pool="sprites" data-key="id" data-pool-cull="100" data-pool-cull-props="x,y,w,h">
    <template>...</template>
</div>

<!-- Custom property names -->
<div data-pool="sprites" data-key="id" data-pool-cull="50" data-pool-cull-props="posX,posY,width,height">
    <template>...</template>
</div>

When only x,y are specified, the pool measures a template clone once at setup to determine default entity dimensions. When w,h are specified, entity size is read from each entity's properties. Use this when entities have varying sizes.

Data-based culling uses display: none for culled entities (removing them from the layout tree entirely), compared to the legacy path which uses visibility: hidden.

Requires data-pool-cull. The data-pool-cull-props attribute must be used alongside data-pool-cull (the padding value). Without data-pool-cull, no culling occurs regardless of cull-props.

data-pool-static

Names an entity property that marks static entities. When this property is truthy on an entity, the pool skips per-frame binding evaluation after the initial add. The entity is rendered once and never re-evaluated. Static entities are also culled only when the camera/viewport changes, not every frame.

<div data-pool="world" data-key="id"
     data-pool-cull="100" data-pool-cull-props="x,y"
     data-pool-static="isStatic">
    <template>...</template>
</div>
// Trees, rocks, flowers: rendered once, never re-evaluated
pool.add({ id: 1, x: 100, y: 200, emoji: 'ðŸŒģ', isStatic: true });

// Bees, butterflies: evaluated every frame
pool.add({ id: 2, x: 50, y: 75, emoji: '🐝', isStatic: false });

This is a major performance optimization for large worlds with many static background entities and fewer moving entities. The flush loop only iterates the dynamic entity array each frame. Static entities are stored in a separate array that is only processed when setCullBounds() reports a viewport change.

data-pool-sort

Automatically sets each entity's z-index based on a numeric entity property. This is essential for 2D games with depth sorting: entities lower on screen (higher Y value) should render on top of entities higher on screen.

<!-- Sort by Y position, ascending (lower Y = lower z-index = behind) -->
<div data-pool="sprites" data-key="id" data-pool-sort="y">
    <template>
        <div class="sprite" data-bind-style="{ left: x + 'px', top: y + 'px' }">
            <img data-bind-attr="{ src: imgSrc }">
        </div>
    </template>
</div>

<!-- Sort descending (higher value = lower z-index = behind) -->
<div data-pool="layers" data-key="id" data-pool-sort="depth:desc">
    ...
</div>
SyntaxBehavior
data-pool-sort="y"Ascending: z-index = Math.round(entity.y)
data-pool-sort="y:desc"Descending: z-index = -Math.round(entity.y)

The sort property is read from each entity on every flush frame, so you can change an entity's depth dynamically by updating the property value.

Pool Definition

A pool is declared inside a component, store, or plugin's pools block. The definition object describes the pool's shape, defaults, and behavior; the framework creates the PoolHandle for you and exposes it as this.pools.name.

wildflower.component('game', {
    pools: {
        enemies: {
            // Each pool has an optional `entity` block: the shape of the
            // entities it holds. Same structure as components and stores:
            // state, computed, methods.
            entity: {
                state:    { hp: 100, maxHp: 100, eState: 'follow' },
                computed: {
                    isDead()    { return this.hp <= 0; },
                    hpPercent() { return Math.round((this.hp / this.maxHp) * 100); }
                },
                takeDamage(n) { this.hp -= n; }
            },

            // Pool-level shared state, visible to every entity's template as props.*
            props: { paused: false },

            // Pool lifecycle hooks. See below.
            onAdd(entity)    { /* ... */ },
            onRemove(entity) { /* ... */ }
        }
    }
})

entity.state

Default fields merged into every new entity at push() time. The merge is shallow: spawn-provided values always win, and any field the spawn omits is filled in from the template.

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

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

Nested objects in the template are shared by reference across entities. If you need per-entity nested objects, pass them at spawn time.

entity.computed

Functions declared in entity.computed become property getters on each entity, with this bound to the entity. Template bindings read computed names the same way they read plain properties.

entity: {
    state: { hp: 100, maxHp: 100 },
    computed: {
        isDead()    { return this.hp <= 0; },
        hpPercent() { return Math.round((this.hp / this.maxHp) * 100); }
    }
}
<template>
    <div data-bind-class="isDead ? 'enemy dying' : 'enemy'">
        HP: <span data-bind="hpPercent"></span>%
    </div>
</template>

Computed properties recompute on every read. There is no cache. Entity mutations are visible to the next binding read with no invalidation step.

Performance note. Each computed is a property getter; it costs roughly 60 Îžs per entity per flush (a function call plus the expression body). Fine for event-driven pools (todos, dashboards, low-to-mid hundreds of entities on user actions). Not fine for hot animation loops at 60 fps with many entities: 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.

Entity methods

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

entity: {
    state: { hp: 100 },
    // Method at top level of the entity block
    takeDamage(n) { this.hp -= n; },
    heal(n)       { this.hp = Math.min(this.hp + n, this.maxHp); }
}

Methods are callable two ways:

// Directly on the entity
this.pools.enemies.get(1).takeDamage(25);
<!-- Via data-action inside the template, the clicked entity is `this` -->
<template>
    <button data-action="takeDamage">Hit</button>
</template>

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 the component method.

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. Inside an entity method or computed, this is the entity itself. The framework doesn't inject a pool handle, pool props, or a parent-component reference into that call site. Module-level values and globals like wildflower.getStore(...) remain reachable through ordinary JavaScript scope. See Entity scope in the guide for the full rule and the common split between entity-local behavior and component-level coordination.

props

Pool-level state shared across all entities in the pool. See the full reference at pool.props.

Lifecycle hooks

onAdd, onRemove, onClear, and onChange fire at the corresponding pool events. See the full reference at pool.onChange and Lifecycle Hooks.

JavaScript API

The PoolHandle exposed on this.pools.name provides the following methods and properties:

pool.push(obj) / pool.push(array)

pool.add is an alias for pool.push. Identical behavior and performance.

Add one or more entities to the pool. Each object must have the key property specified by data-key. Pass a single object for individual adds, or an array for bulk insertion via DocumentFragment (single DOM operation).

// Single add: returns the same object for chaining
const enemy = this.pool('enemies').add({
    id: this.nextId++,
    x: 100,
    y: 0,
    hp: 50,
    maxHp: 50,
    imgSrc: 'goblin.png',
    className: 'enemy'
});
enemy.x += 10; // The returned object IS the entity

// Bulk add: array of objects, single DOM operation
this.pools.enemies.add([
    { id: 1, x: 0, y: 0, hp: 50, className: 'enemy' },
    { id: 2, x: 100, y: 0, hp: 50, className: 'enemy' },
    { id: 3, x: 200, y: 0, hp: 50, className: 'enemy' }
]);

When the first entity is added to any pool (or a component defines tick()), WildflowerJS automatically starts the shared rAF rendering loop. The template is cloned, initial bindings are applied, and the element is appended to the container. Bulk add uses a DocumentFragment so all elements are inserted in a single DOM operation, significantly faster for large collections.

pool.remove(key)

Remove an entity by its key value. Returns true if the entity was found and removed, false otherwise. The entity's DOM element is detached and recycled for reuse by future add() calls (see Entity Recycling).

// Remove enemy with id 42
const wasRemoved = this.pool('enemies').remove(42);

// Common pattern: remove from end of items array in reverse
const pool = this.pool('projectiles');
for (let i = pool.items.length - 1; i >= 0; i--) {
    if (pool.items[i].offScreen) {
        pool.remove(pool.items[i].id);
    }
}
Items array order is not guaranteed after removal. The pool uses O(1) swap-with-last removal internally, which means the items array order may change when entities are removed. Do not rely on insertion order in pool.items after removals.

pool.update(key, props)

Patch an entity's properties by key. Uses Object.assign to merge the provided properties into the entity object. Returns the entity, or null if no entity with that key exists. For live pools, changes are reflected on the next rAF flush. For static pools (data-pool-static), bindings are applied synchronously; the DOM updates immediately.

// Move an entity
pool.update(enemy.id, { x: 100, y: 200 });

// Update multiple properties at once
pool.update(tower.id, {
    level: tower.level + 1,
    damage: tower.damage * 1.5,
    imgSrc: getSprite('tower-upgraded')
});

// Returns null for nonexistent keys
const result = pool.update(999, { x: 0 }); // null

pool.get(key)

Retrieve an entity by its key. Returns the entity object, or undefined if no entity with that key exists.

const enemy = pool.get(42);
if (enemy) {
    enemy.hp -= 10;
}

pool.setCullBounds(left, top, right, bottom)

Set custom cull bounds for data-based culling. Use this for pannable or zoomable worlds where entity coordinates do not map directly to screen position. The bounds define the visible region in entity coordinate space.

// In a pannable/zoomable game world:
function updateCamera() {
    worldEl.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;

    // Tell the pool what region of the world is visible
    pool.setCullBounds(
        -panX / zoom,              // left edge in world coords
        -panY / zoom,              // top edge
        (-panX + viewportW) / zoom, // right edge
        (-panY + viewportH) / zoom  // bottom edge
    );
}

When setCullBounds is not called, the pool derives cull bounds from the container's parent element dimensions automatically. setCullBounds is only needed when a CSS transform (pan/zoom) makes the container larger than the visible area.

The pool tracks whether bounds have changed. Static entities (see data-pool-static) are only re-culled when the bounds actually change, not every frame.

pool.clear()

Remove all entities from the pool. This is a fast operation: it uses replaceChildren() to clear the DOM in one call, then resets the internal data structures.

// Reset the game: clear all entity pools
this.pool('enemies').clear();
this.pool('projectiles').clear();
this.pool('loots').clear();

When all pools across all components are empty after a clear() or remove(), and no components have tick() hooks, the shared rAF loop stops automatically to avoid wasting CPU.

pool.items

The raw array of entity objects. You can iterate this array, mutate entity properties, read values, or use array methods. Changes are reflected in the DOM on the next rAF flush.

// Iterate and mutate
for (const enemy of this.pool('enemies').items) {
    enemy.x += enemy.speedX;
    enemy.y += enemy.speedY;
}

// Find an entity
const boss = this.pool('enemies').items.find(e => e.type === 'boss');

// Check array length
if (this.pool('enemies').items.length === 0) {
    this.nextWave();
}
Do not replace the items array. pool.items is the pool's internal array. Do not reassign it (pool.items = [...]). To add and remove entities, use the add(), remove(), and clear() methods, which keep the DOM in sync.

pool.length

pool.size is an alias for pool.length. Identical behavior.

Returns the current number of entities in the pool. Always accurate, including mid-removal.

if (this.pool('enemies').size > 0) {
    // Still enemies alive
}

// Use for HUD display
document.getElementById('enemy-count').textContent = this.pool('enemies').size;
Pool aggregate reads are not reactive. pool.length and pool.size are plain JavaScript getters; they do not pass through a reactive proxy. A computed property that reads them will evaluate once on first eval (seeing the not-yet-populated pool, typically 0) and will NEVER re-evaluate when entities are added or removed. This is by design — pools intentionally bypass reactivity for performance.

Pattern that silently misbehaves:

// BAD — computed body never re-runs when the pool changes
wildflower.component('hud', {
    computed: {
        count() { return this.pool('enemies').length; }
    }
});

Correct pattern — drive the count from explicit reactive state:

wildflower.component('hud', {
    state: { count: 0 },
    computed: {
        label() { return this.state.count + ' enemies'; }
    },
    init() {
        const p = this.pool('enemies');
        p.add({ id: 1 });
        this.state.count = p.length; // explicit mutation triggers cascade
    }
});

For imperative HUD-style updates (the example above), pool.length is fine because you're writing to the DOM directly — there's no reactive observer expecting to be notified.

Array-like readers

Pools expose a read-only array surface directly on the handle. These delegate to the underlying items array; no wrapper allocation per call.

pool.at(i);                 // entity at DOM-order index i (undefined if out of range)
pool.find(fn);              // first entity matching predicate, or undefined
pool.filter(fn);            // new array of matching entities
pool.map(fn);               // new array of mapped values
pool.forEach(fn);           // iterate, no return value
pool.some(fn);              // true if any match
pool.every(fn);             // true if all match
pool.reduce(fn, initial);   // fold over entities

for (const entity of pool) { ... }   // pool is iterable

Use at(i) when you need positional access; use get(key) when you need key-based access. at reflects DOM order and is stable across removals. The iteration order of find, filter, map, forEach, some, every, reduce, and for...of follows the internal items array, which reshuffles on removal via swap-with-last. Order is not stable, but these methods don't depend on it.

Not exposed: splice, pop, indexOf, and slice are intentionally omitted. They only make sense on arrays with stable index positions, which pools don't provide (swap-with-last removal reshuffles the items array). Use remove(key) to delete and at(i) with DOM-order for positional reads.

pool.recycleSize

Returns the number of DOM nodes currently in the recycle free list, available for reuse by future add() calls. Read-only.

console.log(pool.recycleSize); // 0 initially

pool.add({ id: 1, name: 'A' });
pool.remove(1);
console.log(pool.recycleSize); // 1, DOM node available for reuse

pool.add({ id: 2, name: 'B' }); // Reuses recycled node
console.log(pool.recycleSize); // 0, node was consumed

pool.getElement(key)

Get the DOM element for a specific entity by its key value. Returns undefined if no entity with that key exists. Useful for attaching event listeners, measuring positions, or applying one-off DOM manipulations.

// Flash an enemy red when hit
const el = this.pool('enemies').getElement(enemy.id);
if (el) {
    el.classList.add('hit-flash');
    setTimeout(() => el.classList.remove('hit-flash'), 200);
}

// Measure an entity's screen position
const rect = this.pool('towers').getElement(tower.index)?.getBoundingClientRect();

Lifecycle Hooks

Declare hooks in the pools block to manage external resources tied to entity lifetime:

Hook Arguments When
onAdd (item) After entity added to pool
onRemove (item) Before individual entity removed (not during clear())
onClear (items[]) Once before bulk clear() (skips onRemove)
pools: {
    balls: {
        onAdd: 'createPhysicsBody',     // string → component method
        onRemove: 'destroyPhysicsBody', // string → component method
        onClear: 'clearPhysicsWorld'    // string → component method
    }
}

// Or inline functions:
pools: {
    dots: {
        onAdd(item) { item.created = Date.now(); }
    }
}
onClear fallback: If onClear is not defined, pool.clear() fires onRemove for each item individually. Define onClear when you need efficient bulk teardown without per-item side effects.

Lifecycle

Initialization

Pools are set up during component initialization. When the framework encounters a data-pool element, it:

  1. Extracts the <template> and compiles its bindings into fast evaluator functions
  2. Removes the <template> from the DOM (same pattern as data-list)
  3. Creates a PoolHandle registered under the pool name
  4. Reads optional attributes (data-pool-fps, data-pool-cull, data-pool-sort)

The pool is ready to use in init(). Cache the handle for use in tick():

wildflower.component('my-game', {
    init() {
        this._pool = this.pool('enemies');
        this._pool.add({ id: 1, x: 0, y: 0 });
    },

    // Called automatically each animation frame
    tick(dt) {
        for (const e of this._pool.items) {
            e.x += e.vx * dt;
        }
    }
});

The tick(dt) Hook

Components with a tick method get called once per animation frame by the framework's shared rAF loop. No manual requestAnimationFrame setup or teardown is needed.

  • dt: milliseconds since the last frame (clamped to 250ms to prevent spiral-of-death after tab switches)
  • now (second argument): the performance.now() timestamp for this frame
  • tick runs BEFORE pool flush; entity mutations made in tick are visible in the DOM on the same frame
  • Works with or without pools, useful for canvas, Three.js, or any per-frame logic
  • Automatically stops when the component is destroyed

Cleanup

Pools and tick hooks are automatically cleaned up when the owning component is destroyed. All entities are removed, DOM elements are cleared, the tick callback is unregistered, and the rAF loop stops if no other pools or tickable components remain active. You do not need to manually clean up pools or cancel animation frames.

Multiple Pools

A component can have any number of pools. Each pool has its own name, template, and configuration. All pools share the same rAF loop, with no overhead from having multiple pools.

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

    <div data-pool="enemies" data-key="id" data-pool-sort="y" data-pool-cull="100">
        <template>...</template>
    </div>

    <div data-pool="projectiles" data-key="id">
        <template>...</template>
    </div>

    <div data-pool="damage-numbers" data-key="id" data-pool-fps="30">
        <template><span class="dmg" data-bind="text" data-bind-style="{ left: x + 'px', top: y + 'px', opacity: alpha }"></span></template>
    </div>
</div>

Typical game architecture uses separate pools for different entity types, each with appropriate performance settings. Background particles at 15fps, enemies with culling and depth sorting at 60fps, damage numbers at 30fps.

Event Handling

Pool templates do not support data-action on individual items. Instead, use data-pool-action on the container to delegate events to pool items:

data-pool-action

Adds delegated event listeners on the pool container. Supports multiple declarations separated by semicolons, with optional CSS selector targeting for different elements within a pool item.

Basic Usage

<div data-pool="enemies" data-key="id" data-pool-action="click:onEnemyClick">
    <template>
        <div data-bind-class="className"
             data-bind-style="{ left: x + 'px', top: y + 'px' }">
            <span data-bind="name"></span>
        </div>
    </template>
</div>
onEnemyClick(item, event) {
    // item is the plain entity object from pool.items
    item.hp -= 10;
    if (item.hp <= 0) {
        this.pool('enemies').remove(item.id);
    }
}

Multiple Actions with Selectors

Use the extended syntax to dispatch to different methods based on which element was clicked within a pool item:

<div data-pool="tasks" data-key="id"
     data-pool-action="click:.edit-btn:onEdit; click:.delete-btn:onDelete; mouseover:onHover">
    <template>
        <div class="task">
            <span data-bind="title"></span>
            <button class="edit-btn">Edit</button>
            <button class="delete-btn">Delete</button>
        </div>
    </template>
</div>
onEdit(item, event)   { /* clicked .edit-btn */ },
onDelete(item, event) { /* clicked .delete-btn */ },
onHover(item, event)  { /* mouseover anywhere in item */ }

Format Reference

Each declaration follows one of three formats, separated by semicolons:

  • method: click (default event), any element in item
  • event:method: specified event, any element in item
  • event:.selector:method: specified event, only when click target matches CSS selector

When multiple declarations share the same event type, selector-specific handlers take priority over catch-all handlers. If a click matches .edit-btn, the catch-all handler is not called.

Implementation: One DOM listener per event type (not per declaration). The framework walks from event.target up to the container's direct child to find the entity, then matches selectors within the entity's DOM subtree. Clicks on the container itself (not on an item) do not fire any handler.

pool.onChange

A callback property that fires synchronously on add(), remove(), and clear(). The pool's size is already updated when the callback fires.

init() {
    const pool = this.pool('enemies');
    pool.onChange = (p) => {
        // Update a HUD counter when entities are added or removed
        document.getElementById('enemy-count').textContent = p.size;

        // Show/hide empty state
        document.getElementById('no-enemies').style.display = p.size === 0 ? '' : 'none';
    };
}

Set pool.onChange = null to remove the callback. There is no overhead when no callback is set.

pool.props

A plain object of shared data available to all pool item expressions via the props. prefix. Declared in the pools block and accessible at this.pools.name.props. Changes to props are picked up on the next rAF flush (or immediately for static pools via update()).

// Declare props in component definition
pools: {
    boids: {
        props: { shape: 'arrow', showLabels: true }
    }
}

// Read/write at runtime
this.pools.boids.props.shape = 'circle';  // all items update on next flush
console.log(this.pools.boids.props.showLabels);  // true
<!-- Use in template expressions with props. prefix -->
<div data-pool="boids" data-key="id">
    <template>
        <div data-bind-class="className + ' ' + props.shape"
             data-bind-style="{ left: x + 'px', top: y + 'px' }">
            <span data-bind="name" data-show="props.showLabels"></span>
        </div>
    </template>
</div>

Use props for values shared across all items: themes, palettes, locale settings, permission flags, display modes. This avoids stamping the same data onto every item object: one object for the entire pool, zero per-item memory cost.

Pools without a props declaration have an empty props object by default.

Continue Learning: For performance optimization techniques including CSS tips, the SVG pre-rasterization pattern, and profiling advice, see Pool Performance.