WildflowerJS Reactive JS, No BS*

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

Latest release: v1.3.0 · see what's new
<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 WildflowerJS.

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 verified 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 Compromise

WildflowerJS doesn't compromise performance for ease-of-use. Even with no build step, on the js-framework-benchmark, WildflowerJS performs at the level of frontier frameworks, level with the fastest signal-based frameworks across list creation, updates, selection, swaps, and removal. And for per-frame workloads, data pools lead every framework we tested in our Lorenz attractor simulation demo.

The charts here are the overall geomean standings and the operation breakdown from our latest full-field run, plus the sustained frame rate from our per-frame animation sweep. Click any chart to see it full size.

Delivery is fast too, because there's less to deliver. One file, no framework runtime split across chunks, no hydration pass. Lighthouse scores hold their own against compiled frameworks without a single build artifact.

Simplicity in the interface, performance in the implementation. WildflowerJS doesn't trade one for the other.

Benchmark setup for these charts: js-framework-benchmark operations 1 through 9, 15 samples per cell, all frameworks in a single run; total-duration medians, lower is better. The frame-rate chart runs each framework's fastest variant on the Lorenz attractor for 8 seconds per particle count, fullscreen on a 120 Hz panel; higher is better. Apple M5 Pro, 24 GB RAM, macOS 26.5.2, Google Chrome 150 (stable, headed).

Bar chart of geomean slowdown versus the fastest framework per operation, vanilla JS baseline 1.00: WF-pool 1.054, Vue Vapor 1.056, Solid 1.095, WF 1.120, Svelte 1.165, Vue 1.263. Lower is better.
Geomean slowdown vs fastest per operation. Lower is better.
Grouped bar chart of all nine js-framework-benchmark operations for Solid, Svelte, Vue, Vue Vapor, WF, and WF-pool, with per-operation rankings. WF-pool is fastest on most operations.
All nine operations, side by side. Stars mark the fastest.
Line chart of sustained FPS versus particle count on the Lorenz attractor for Solid, Svelte, Vue, Vue Vapor, WF, and WF-pool. WF-pool holds the highest frame rate at every count, staying above 60 FPS past 4500 particles.
Per-frame animation. Sustained FPS as particle count grows; higher is better.

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.

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.

Live Server Data: Built In, Stays True

With WildflowerJS SSR, the page arrives already true. The server (your server, whatever back-end you prefer) renders your data into real HTML, so the first paint is real content, indexable and readable before a line of JavaScript runs. And because the markup is genuine HTML, hydration reads the page's state straight back out of the document. Server-rendered components end up exactly equivalent to client-rendered ones.

v1.3 brings data-query, which does for the rest of the page's life what SSR does for first load. Most frameworks hand you fetch() and leave the rest to you. There's an entire ecosystem of client data libraries that exists to fill that gap. WildflowerJS makes it a declaration instead. Name a source, point an element at it, say how fresh it should stay. Loading and error states, refresh on demand, request racing, and the whole refresh ladder (poll, conditional GET, focus, reconnect, server push) come with it. Oh, and there's no query language. Refinement is an ordinary computed property, and filtering happens client-side without a network round trip.

Together, Wildflower's SSR and data-query tell one story. The server renders the page with real data. Because hydration reads the page itself, there's no flash of empty content, no loading spinner over data the user can already see, and no hydration scripts locking up the main thread. The server's render is the actual UI. When paired with data-query, your SSR becomes the first result of a standing query. The query adopts that markup and keeps it updated from there.

And as you can see in the example, your markup is 100% HTML. From bean to cup, what you see is what you get.

<div data-component="product-board">
  <p data-show="$products.isLoading">
    Loading…
  </p>
  <p data-show="$products.error">
    Failed.
    <button data-action="retry">Retry</button>
  </p>

  <span data-bind="$products.count"></span>
  products

  <tbody data-query="products">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="stock"></td>
      </tr>
    </template>
  </tbody>
</div>
// The entire data layer:
wildflower.query('products', {
  from: '/api/products',
  key: 'id',
  refresh: ['focus', 'etag:60']
});

// Server-rendered page? Add data-ssr="true"
// and the markup the server sent becomes the
// query's first result. Live from there.

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.

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.

Error Codes

Reference for all WF-* error codes. Click a category button below to filter.

Showing:

WF-001 Root element not found
Core

Framework initialization cannot find the specified root DOM element. Check that your mount target exists in the HTML before calling wildflower.start().

WF-002 Invalid configuration value
Core

A configuration attribute carries a value the framework does not recognize, so the default is used instead. The warning names the attribute and the accepted values (for example data-error-handling accepts log, throw, or silent). Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-003 Capability excluded from this build tier
Core

A definition or markup uses a capability this build tier excludes, so it silently does nothing: tick() defined in a build without the frame loop (pool module excluded), or scoped-slot read bindings in the nano tier. Switch to a build that includes the capability, or remove the usage. The warning names the specific capability and the tier boundary. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-101 Error initializing component
Components

A component's init() method or setup logic threw an exception. Check the browser console for the underlying error.

WF-102 Component instance not found
Components

Attempting to access a component that doesn't exist in the registry. Verify the component name matches its data-component attribute and that it has been registered.

WF-103 Component context not available
Components Context

A component's context object is missing when required. This usually indicates the component was destroyed or not fully initialized.

WF-104 Error in parent event handler
Components Actions

A parent component's event handler threw an exception when invoked by a child. Check the parent's method for errors.

WF-105 Manual DOM write on an engine-owned node
Components

A component called .text() on a data-bind node, .html() on a data-bind-html or data-list node, or .remove() on a managed node through the $el() helper. The engine keeps those nodes current, so the manual write will be overwritten by the next update or leave the engine tracking a removed node. Update state instead and let the binding do the writing. Unmanaged nodes stay free, and .val() on a data-model input is the sanctioned bridge. Warning severity (console.warn, never throws); fires when debug mode is on (the default in development builds).

WF-106 destroy() with the element still in the document
Components

destroyComponent() ran while the component's element was still connected, so the next scan will auto-resurrect it as a fresh instance with init() re-fired. For a real teardown remove the element as well (instance.element.remove()); if you wanted a reset, re-initialize state instead. Removing the element without calling destroy is always safe: the engine garbage-collects instances whose elements leave the document. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-107 Declared provider never provided
Components

A definition lists a key in uses: that nothing ever registered, so the $name accessor is never attached and reads of it are undefined. Register the provider with wildflower.provide('name', value) before components that use it initialize, or fix the key if it is a typo. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-108 Directive or plugin registration overwritten
Components

A directive or plugin was registered under a name that already exists, and the new registration replaced the original. If both registrations are intentional (hot reload, deliberate override) the warning can be ignored; otherwise rename one. Note the asymmetry with components and stores, where a conflicting re-registration keeps the original instead (WF-215). Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-201 Error evaluating computed property
State

A computed property function threw an exception during evaluation. Check that all referenced state properties exist and have expected types.

WF-202 Circular dependency detected
State

Two or more computed properties reference each other, creating an infinite loop. Restructure your computed properties to break the cycle.

WF-203 Error setting state value
State

The reactive proxy's set trap encountered an error. This can happen when assigning invalid values or when the state object has been corrupted.

WF-204 Error deleting state value
State

The reactive proxy's delete trap encountered an error when removing a state property.

WF-205 Error loading state from storage
State

Failed to read persisted state from localStorage or sessionStorage. The stored data may be corrupted or the storage quota exceeded.

WF-206 Error saving state to storage
State

Failed to persist state to localStorage or sessionStorage. Check that the storage quota has not been exceeded and that the data is serializable.

WF-207 Invalid parameter for state update
State

A state update method received an argument of the wrong type. Verify you're passing the correct data type.

WF-208 Computed property does not exist
State

Retired in v1.3. This code was reserved for reads of undefined computed properties but never fired from any code path. Misspelled computed and state references are caught by binding validation in development builds, which warns with a did-you-mean at bind time. The code number is not reused.

WF-209 Computed property must be a function
State

A computed property was defined as a value instead of a function. Computed properties must be functions that return a value.

WF-210 Invalid path segment
State Bindings

A dotted path like user.profile.name contains an invalid segment. Check for typos or undefined intermediate objects.

WF-211 Error in subscription callback
State Stores

A user-provided subscription callback threw an exception. Check the function passed to subscribe().

WF-212 Pool aggregate read inside a computed
State

Retired in v1.3. Pool aggregates (pool.length, pool.size) are reactive on demand as of v1.3: a computed reading them re-evaluates when entities are added, removed, or cleared, so the trap this warning guarded no longer exists and the warning was removed. On v1.2 and earlier, aggregates bypass reactivity and a computed reading them evaluates once and goes silently stale; the workaround there is mirroring the count into reactive state inside a tick(). The code number is not reused.

WF-213 Watch/subscribe path targets a list item by numeric index
State

A watcher or subscription registered a path like items.0.name. This is an anti-pattern: reactivity tracks items by object identity, so the index in a change path reflects the item's position when it was first observed — after a splice, removal, or reorder the watcher fires for the wrong slot or goes silent. Watch the array (or a computed over it) and track items by id instead, e.g. watch: { items() { ... } } or a computed like activeItem() { return this.items.find(i => i.id === this.selectedId) }. Warning severity (logged via console.warn, never throws); dev-mode only, stripped from production builds.

WF-214 Zero-arg computed in a list row reads an item property via this
State Bindings

A computed referenced inside a data-list row template was declared with no parameters, and its body reads this.<prop> where <prop> is not on the component's state or computeds but is a property of the current list item. Zero-arg computeds evaluate at component scope, so that read silently resolves undefined. Item-level computeds receive the item as their first argument: declare it, e.g. priceLabel(item) { return '$' + item.price }. A zero-arg computed that reads only component state is legitimate inside a row and never triggers this warning. Fires once per component and computed; warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-215 Component or store re-registered with a different definition
State Components

A component or store was registered under a name that already exists, and the incoming definition differs from the stored one. Registration is first-write-wins, so the original is kept and the new definition is ignored. This is almost always an accidental collision (two components sharing a name, a hot reload without teardown). To replace a definition intentionally, call wildflower.unregister('<name>') first, then re-register; or give the new one a distinct name. The comparison hashes method source, so two definitions that share method names but differ in a method body are still flagged; an identical re-registration (the same definition scanned twice) does not warn. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-216 State property read thousands of times per frame (sustained hot loop)
State

One state property is being read through the reactive facade at hot-loop rates, sustained across animation frames. A facade read costs roughly 100x a plain property read; that is proxy physics, and every fine-grained framework pays it. The fix is one line: hoist the value to a local before the loop (const speed = this.state.speed) and read the local inside it. For per-entity hot data, pool entities are plain objects with zero proxy cost. One-shot sweeps (building a large structure once during init) do not trigger this warning; it fires only for reads that recur frame after frame, and only once per property name. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-217 Computed wrote to state during evaluation
State

A computed mutated tracked state while it was being evaluated, including an in-place sort or reverse. Computeds must be pure. The mutation invalidates the computed while it runs, and anything bound to it (a data-list, a binding) can silently render empty or stale. Copy before mutating (return [...items].sort(...)), or move the write into a method. Writes inside untrack() are the sanctioned escape and stay silent. Warned once per computed. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-218 Name collision across definition buckets
State

The same name is defined in more than one bucket of a definition: a method colliding with a state key or a computed, or a key defined in both state and computed. One of them is shadowed wherever the bare name resolves. For state/computed collisions the computed wins everywhere except explicit this.state.key reads. Rename one. Warned once per definition. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-219 Definition key ignored
State

A top-level key in a component, store, plugin, or pool entity definition is not a function and is not part of the definition contract, so it was ignored. State values belong inside state: {}; only methods live at the top level, never in a methods or actions block. Underscore-prefixed keys are the deliberate stash and stay silent. Warned once per definition with a hint for the specific key. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-220 Assignment to a computed property
State

Computed properties are read-only derived values. The assignment was ignored and the property keeps computing from its inputs. Store the value in state instead, or rename the computed if you meant a state field. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-221 batch() called without a function
State

wildflower.batch(fn) groups writes into one flush and requires a function argument; the call was a no-op. Pass the writes inside a function: wildflower.batch(() => { ... }). Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-301 Error resolving data in context
Context Bindings

Context data resolution failed during binding evaluation. The bound property path may be invalid or the data structure has changed unexpectedly.

WF-302 Missing component instance in context
Context Components

A context operation requires a component instance but none is available. The component may have been destroyed.

WF-303 Error updating context
Context

The context update process encountered an exception. This is typically caused by invalid data or a corrupted context state.

WF-304 Error in context dependency notification
Context

Failed to notify dependent contexts of a data change. A dependent binding or computed property may have an error.

WF-401 Template not found for list
Lists

A data-list element resolved no item template. The development build names the cause it found: row markup written as direct children instead of inside a <template>, a container inside <svg> where the HTML parser removes <template> elements before any script runs, or no template in any searched source (inline child, data-use-template reference, inherited templates). Add a <template> child, or for SVG bind a fixed set of elements instead.

WF-402 Error rendering list
Lists

The list rendering process threw an exception. Check that the list data is a valid array and that template bindings reference valid item properties.

WF-403 Error updating list item
Lists

Updating an existing list item's bindings failed. The item data may have an unexpected structure.

WF-404 Error removing list item
Lists

Removing a list item from the DOM failed. The element may have already been removed or detached.

WF-405 Error in append optimization
Lists

The optimized append path for adding items to the end of a list encountered an error. The framework will fall back to a full re-render.

WF-406 Error in swap optimization
Lists

The optimized swap path for reordering list items encountered an error. The framework will fall back to a full re-render.

WF-407 Error in sparse update optimization
Lists

The optimized sparse update path (updating a subset of list items) encountered an error. The framework will fall back to a full re-render.

WF-408 data-pool container name is not in the component's pools block
Pools Components

A data-pool container names a pool that does not appear in the component's declared pools: {} block. Pool names must match exactly; code that populates getPool('items') never reaches a container written as data-pool="itmes", so the container renders nothing. This is almost always a typo, and the warning suggests the closest declared name. A markup-only pool (no declaration, populated programmatically by the exact same name) is legitimate and stays silent. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-409 Pool has a container but was never populated
Pools Components

A data-pool container and its template are wired correctly, but nothing was ever added to the pool by the time the page settled, so nothing renders. Populate it from the component with this.getPool('name').add({ id: 1, ... }) or through the pools.name handle. If the pool fills later by design (for example on user interaction), this note can be ignored; it fires once and only in development builds.

WF-410 Entity spawns produce mixed shapes (hidden-class deoptimization)
Pools

Two spawn paths in one pool produced entities with different fields or a different field order. V8 gives a pool one fast hidden class only when every entity shares the same shape; once shapes diverge, every hot-loop read in the pool slows down. This is platform physics rather than a framework rule. Make all spawn paths build entities with the same fields in the same order: initialize missing fields up front (null or 0), or split differently-shaped entities into separate pools. Fields filled by entity.state defaults count as normalized and stay silent. Fires once per pool, showing both shapes. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-411 entity.computed pool reached a frame-budget size
Pools State

A pool that declares entity.computed properties reached 200 entities. Entity computeds are uncached by contract (they re-evaluate on every read, roughly 60us per entity per flush measured), so on a per-frame pool this cost lands on every animation frame. For per-frame pools at this scale, store the derived value as a plain data field updated on mutation, or mark non-animating entities static with data-pool-static="prop". Passive pools (data-pool-static on the container) never flush per frame and stay silent. Fires once per pool. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-412 Named or typed template never resolved
Lists

A template lookup by name or type found nothing to render, or found something unusable: a configurable template missing from the hierarchy with no fallback, a target component that does not exist, a polymorphic item type with no matching data-type template and no default, a template with empty content, or duplicate item-template names (the first wins). The warning names the template or type it searched for. Check the name against the defining component, or add a fallback/default template. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-414 Index-dependent method or direct mutation on pool storage
Pools

Pools use swap-with-last storage, so positions reshuffle on every removal and index-based operations would hit a different entity than intended. In dev builds pool.splice(), pool.pop(), pool.indexOf(), and pool.slice() exist as throwing stubs that explain this, and mutating pool.items directly is caught by a consistency check at the next API call. Iterate pool.items freely; mutate only through the pool API (add/remove), use remove(key) to delete and at(i) for stable DOM-order access. Production keeps the raw array (pull mode's zero-overhead contract). Dev-mode only.

WF-415 Pool entity key missing or duplicate
Pools

An entity added to a keyed pool is missing the declared key property, or carries a key the pool already holds; the entity is not registered. Give every entity a unique value for the pool's key property before adding it. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-501 Error evaluating binding expression
Bindings

A data-bind expression failed to evaluate. Check for typos in property names or invalid JavaScript expressions. Also emitted as a warning when store shorthand ($store.path) is used in data-model.

WF-502 Error evaluating class binding
Bindings

A data-bind-class expression failed to evaluate. Check that the expression returns a valid string or object.

WF-503 Failed to create HTML binding context
Bindings

Creating a context for a data-bind-html binding failed. Check that the binding path is valid.

WF-504 Error updating conditional context
Bindings

Updating a data-show or data-render conditional context failed. Check that the bound expression evaluates to a boolean-like value.

WF-505 Class binding shape mismatch
Bindings

A data-bind-class binding received a value that is not a string. The element-level path expects a space-separated class string. Inline expressions can use the {'class-name': condition} object form, but a computed property should return the resolved string itself. The framework coerces the value (truthy keys joined to a string, or the value stringified) so the page keeps rendering, but the underlying mismatch should be fixed in your code.

Wrong: computed: { classes() { return { 'is-active': this.active }; } }
Right: computed: { classes() { return this.active ? 'is-active' : ''; } }

WF-507 data-prop path unresolvable in the parent
Bindings Components

A data-prop-* (or data-props) value looked like a path, resolved to undefined, and was still absent from the parent's state, computed properties, and methods after the page settled. A typo'd path is indistinguishable from a real prop at resolution time, so the check waits out the init window first; a parent that sets the key in init() stays silent, as does a prop whose declared default absorbed the miss. The warning suggests the closest matching parent name. If the parent genuinely provides the value later (for example after a fetch), the note can be ignored. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-508 Prop attribute names a prop the component never declared
Bindings Components

A data-prop-* attribute (or a key inside data-props) was passed to a component that never declared that prop, so the value is never read. Only declared props are consumed; this is the prop-name sibling of WF-507's path typo (data-prop-titel against a declared title, or data-prop-user-id against userId). When the component declares props, the warning suggests the closest declared name; when the component has no props block at all, every prop attribute on it is dead and the warning shows the declaration to add (props: { title: { type: String } }). Fix the attribute name or declare the prop. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-509 Binding validation
Bindings

A binding references a name that does not resolve: a data-bind/data-model/data-show/data-render path, an identifier inside a class or style binding expression, a nested path segment, a type hint that does not match the value, or a data-action method that does not exist on the component. The development build warns at bind time with a did-you-mean and the list of available names. On by default in development builds; disable with debug: false. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-510 data-props attribute failed to parse
Bindings

The bulk data-props attribute could not be parsed as JSON, so no props were passed. Common causes are single quotes instead of double quotes around keys and strings, or unescaped quotes inside values. For dynamic values, prefer individual data-prop-* attributes with paths. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-601 Error in action handler
Actions

A data-action handler method threw an exception. Check the method referenced in your data-action attribute.

WF-602 Error in component method
Actions Components

A component method execution failed. Check the method for runtime errors such as accessing undefined properties.

WF-603 Cannot emit: component instance not found
Actions Components

emit() was called but the component instance could not be located. Ensure the component is mounted and initialized.

WF-604 data-action targets a reserved lifecycle name
Actions

A data-action points at init, tick, destroy, or another lifecycle hook. Lifecycle names run on the framework's schedule, not on events: tick() runs every animation frame, destroy() tears the component down, and with the element still in the DOM the next scan auto-resurrects it. Rename the handler to a specific verb (increment, handleClick, refresh). Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-605 Stale event API in a replayed action
Actions

An action fired before init() finished was queued and replayed afterward, and the handler then called preventDefault(), stopPropagation(), or stopImmediatePropagation() on the original event. By replay time the browser has already processed the event, so the call is a no-op. Put data-event-prevent on the element to block the default reliably; the framework intercepts before user code runs. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-606 $entity.path in data-action
Actions Stores

$entity.path is a read accessor for external state and cannot name an action handler; actions are component-locked by design. To delegate to another entity's method, define a one-line wrapper on the component: bump() { this.getStore('name').bump(); }. The warning shows the exact wrapper for the path you wrote. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-604 Cannot emit: component context not available
Actions Components

emit() was called but the component's context is unavailable. The component may have been destroyed.

WF-701 Route not found
Routing

Navigation attempted to access a route that hasn't been defined. Check your route configuration.

WF-702 Target route not found for alias
Routing

A route alias points to a target route that doesn't exist. Verify the alias target matches a defined route path.

WF-703 Error in route guard
Routing

A navigation guard function (beforeEnter, beforeLeave, etc.) threw an exception. Check the guard function for errors.

WF-704 Navigation queue exceeded retry limit
Routing

The navigation queue has exceeded its maximum retry attempts. This usually indicates a redirect loop in your route guards.

WF-705 Named route not found
Routing

Navigation by name references a route that doesn't exist. Check the name property in your route definitions.

WF-706 Invalid route configuration
Routing

A route definition has an invalid structure. Routes require at minimum a path property.

WF-707 Router already initialized
Routing

Attempting to initialize the router more than once. The router should only be configured and started once per application.

WF-708 No route matched for path
Routing

No route pattern matches the requested URL path. Consider adding a catch-all route (path: '*') for 404 handling.

WF-709 Error in route handler
Routing

A route's handler function threw an exception during execution.

WF-710 Error loading route component
Routing

An async/lazy-loaded route component failed to load. Check the network request and module path.

WF-711 Error in scroll behavior
Routing

The scroll restoration or positioning function threw an exception after navigation.

WF-712 Error in route lifecycle hook
Routing

A route lifecycle hook (beforeEnter, afterEnter, etc.) threw an exception.

WF-801 Error during SSR activation
SSR

Server-side rendered component activation failed. The server-rendered HTML may not match the expected component structure.

WF-802 Error during hydration
SSR

Hydrating server-rendered HTML encountered an error. Ensure the server-rendered markup matches the client component's expected structure.

WF-901 Store component name must be a string
Stores

wildflower.store() was called with a non-string first argument. The store name must be a string.

WF-902 Store component definition must be an object
Stores

wildflower.store() was called with a non-object second argument. The store definition must be a plain object.

WF-903 Error in store init hook
Stores

A store's init() lifecycle hook threw an exception. Check the store's initialization logic.

WF-904 Error creating store component
Stores

The store creation process failed. Check the store definition for structural errors.

WF-905 Error in external() accessing store
Stores Components

external() failed when accessing a store. Verify the store name and property path are correct.

WF-906 Error in store subscription callback
Stores

A store subscription callback threw an exception. Check the function passed to the store's subscribe() method.

WF-907 Failed to create default app-store
Stores

Automatic creation of the default application store failed. This is an internal initialization error.

WF-908 Store path written from inside its own notification
Stores

An onStoreUpdate handler wrote the store path it was being notified for, which would loop forever. The engine drops the nested notification and keeps the write, so the cycle cannot hang the page; the warning names the store and path. Derive the value with a computed instead, or write a different path. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-909 Subscribed or watched store never registered
Stores

A component subscribes to, watches, or path-subscribes a store name that nothing ever registered. The miss is reported (error severity for subscribe:, warning for watchers) and init continues best-effort so the rest of the component works. Fix the name, or register the store before the component initializes. Dev-mode only, stripped from production builds.

WF-910 Timed out waiting for a subscribed store
Stores

A subscribed store exists but did not become ready within the wait window (default 5000ms; per-component override via subscribeTimeout in the definition). The component's onError hook receives the timeout and init continues best-effort. Check the store's async init() for work that never resolves. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-950 External write to a query-owned store
Queries

A field on a query's backing store (rows, isLoading, isStale, error, syncError, lastSync) was assigned from application code. Query stores are engine-owned: the fetch pipeline rewrites them on every sync, so an application write survives only until the next refresh replaces it. The supported pattern is to mutate the data source, then call wildflower.getQuery('name').invalidate(). The write still lands, since a deliberate optimistic update is legitimate; the warning fires once per store. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-951 Query name already registered
Queries

A second wildflower.query() call used a name that is already registered. The second registration is ignored and the existing query's handle is returned, so config changes in the second call never apply. Declare each query once, at module scope. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-952 Query name collides with an existing store
Queries

Queries and stores share one entity namespace, because every query is backed by a store of the same name. A wildflower.query() call whose name matches an existing store is refused (null is returned) rather than silently taking over the store's data. Pick a name no store uses. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-953 Query from is not a URL or function
Queries

The from option must be a URL string or a function returning the data (or a Promise of it). Registration is refused (null is returned). There are no other source types: anything beyond a URL is expressed as a function. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-954 Sub-second poll rung
Queries

A numeric refresh rung below 1 is almost always a units mistake: poll values are seconds, so 0.5 means twice per second (120 requests per minute against the source), where the author usually meant "every 30 seconds". The rung still runs as declared. If sub-second polling is genuinely intended, the warning can be ignored. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-955 No such query is registered
Queries

Fires in two shapes that share this code: a data-query attribute in markup names a query that was never declared (the element is left untransformed and never activates), or getQuery('name') is called for an unregistered name (returns undefined). Register the query with wildflower.query('name', { from: ... }) before the markup mounts, and check for typos between the attribute and the declaration. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-956 data-seed is not valid JSON
Queries

A data-seed attribute on an adopted row or query element could not be parsed as a JSON object, so its fields were ignored; the row keeps only what the display text parse recovered. Common causes are single quotes inside the JSON, unquoted keys, or a non-object value (arrays are ignored by design). Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-957 The sse rung needs a stream URL
Queries

The 'sse' rung opens an EventSource, which requires a URL. When from is a URL string it doubles as the stream endpoint, but when from is a function there is nothing to connect to, so the rung is skipped (other declared rungs still run). Add a stream: option naming the SSE endpoint. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-958 SSE message was not valid JSON
Queries

The stream contract: a JSON message body is applied as the new result, an empty message is an invalidation signal (conditional refetch). A non-empty message that fails to parse as JSON fits neither, so the engine degrades it to an invalidation and refetches, keeping data correct at the cost of one extra request. Fix the server to send JSON bodies or empty invalidation pings. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-959 Record query resolved null
Queries

A record-shaped query's source resolved to null or undefined. This is a valid empty context by design: bound fields render empty and nothing throws, matching "no result yet" states like a logged-out session. The warning exists because a permanently null record often means the source returns a wrapper shape ({ data: {...} }) rather than the record itself. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-960 Append received rows without the declared key
Queries

An append refresh (refresh({ params, append: true })) received rows that lack the query's declared key field. Accumulation identifies rows by key to dedupe and merge, so keyless rows would duplicate endlessly; the engine applies the result as a plain replace instead and warns. Give appended rows the declared key, or declare the key the source actually returns. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-961 refresh() received unexpected options
Queries

refresh() takes an options object with exactly two recognized fields, params and append. Any other key is almost always request parameters passed directly (refresh({ status: 'open' })), which the engine cannot distinguish from an option name and therefore never sends. Nest them: refresh({ params: { status: 'open' } }). Keeping parameters inside params is what guarantees no request parameter can ever collide with an option name. The refresh still runs, without the stray values. Warning severity (console.warn, never throws); dev-mode only, stripped from production builds.

WF-CSP-SYNTAX Cannot parse expression
CSP Bindings

The CSP-safe expression parser encountered syntax it cannot parse. Simplify your binding expression or check for syntax errors.

WF-CSP-UNSUPPORTED Expression uses unsupported syntax
CSP Bindings

The expression contains a syntax construct not supported by the CSP-safe expression parser. The parser supports literals, identifiers, member access, binary/logical/unary/conditional expressions, array expressions, and function calls. Anything else (arrow functions, template literals, object literals, destructuring, assignment, etc.) triggers this error. Simplify the expression or move the logic into a computed property or component method.

WF-CSP-SECURITY Blocked access to restricted API
CSP

CSP security policy blocked one of: (1) a blocked global identifier (window, document, eval, Function, fetch, setTimeout, and others); (2) a blocked property access (__proto__, prototype, constructor); or (3) a function call other than external(). In CSP mode, only external() is permitted as a function call in binding expressions.

WF-SEC-BLOCKED Dangerous attribute or URL value blocked
Security

A binding tried to write a value the security layer refuses by design: a javascript: or vbscript: URL, a scriptable data: URI in a URL attribute (raster image formats are allowed), or a blacklisted attribute such as inline event handlers. The write is dropped in every build; the warning fires in dev builds. This is a policy outcome, not an error. If you control the value, use a safe scheme; if the value is user-supplied, the block is doing its job.

WF-SEC-SANITIZER HTML rendered without a configured sanitizer
Security

A router outlet is rendering HTML with no sanitizer configured. If the HTML can ever include user-supplied content, configure one with wildflower.setHtmlSanitizer() (for example DOMPurify) to prevent XSS. For fully static, author-controlled HTML the notice can be ignored. Warning severity (console.warn, never throws).

WF-EFFECT Error resolving path in render effect
State Bindings

The render effect system failed to resolve a binding path during a reactive update. The bound property may not exist on the component's state.