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 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 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.

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.

Advanced Reactivity

A tour of the engine. What actually happens between this.foo = 1 and your DOM updating, and the runtime decisions the framework makes along the way.

Audience: framework users who want to understand why their code behaves the way it does. Most code never needs this level of detail. Everything on this page is informational, not prescriptive.

The basic Reactivity page covers what you need to write code that works. This page covers what the engine does underneath: the dependency graph it builds as your code runs, how a single mutation reaches the DOM, and the failure modes the design is built to prevent.

1. One engine, four entity types

Components, stores, plugins, and pools all share the same reactive engine. There is one dependency-graph core, and each component, store, plugin, or pool gets one handle onto it. The engine's behavior is identical across the four kinds.

The four entity types (component, store, plugin, pool) all share the same reactive core

One terminology note about pools, because the framework overloads the word "entity": a pool is one of the four entity-types and gets one handle on the engine. The items inside a pool are also called entities (per the per-entity declaration shape: entity: { state, computed, methods }) but those items share the pool's single handle rather than each having their own. A pool with 2600 boids is one handle, not 2600. That is the whole point of pools: high-frequency rendering at scale, without paying for thousands of independent reactive handles.

This architectural decision is one of the framework's structural strengths. Each kind wraps the same engine in slightly different lifecycle hooks: a component has DOM, a store does not, a pool has many items sharing one renderer and one handle. But the proxy traps, the effect scheduler, the computed evaluator, and the cross-entity bridge are the same code for all four.

Practical consequence: anything you learn about how a component reacts applies unchanged to a store, a plugin, or a pool. Computed properties validate the same way. Effects schedule the same way. Cross-entity reads work the same way.

Why it matters: the reactive graph core plus the per-entity handle is the entire reactive system. There is no separate code path for stores, no different proxy for plugins. The single-implementation property is what makes the framework possible to learn end-to-end.

2. The proxy in the middle

Your state object is a JavaScript Proxy. Every read passes through the get trap; every write passes through the set trap. Those two traps are where reactivity is implemented.

Get trap records a dependency edge; set trap wakes the observers of the value

The get trap does dependency tracking. When code inside an effect or a computed property reads this.user.name, the get trap records an edge: the currently-running effect depends on that value. The edge is what lets the framework re-run the effect later if the value changes.

The set trap does notification. When code writes this.user.name = "Bob", the set trap updates the value and wakes the observers of that value: the effects and computed properties that read it. Nothing else is touched. There is no "which properties changed" scan, because the property that changed is the one being written.

The two traps together form a dependency graph that is built dynamically by code execution. There is no static analysis step, no compiler, and no manual useState-style declaration of what depends on what. The graph is implicit in the reads and writes your code performs.

Subtle point: the get trap only records an edge when an effect or computed is currently executing. Reads outside any reactive context (for example, in a console log or a regular method called manually) pass through cleanly without recording anything. This is what makes the framework work without explicit subscribe/unsubscribe ceremony.

3. How a write reaches the DOM

A write to this.X does two things synchronously inside the set trap: it updates the value, and it marks the observers of that value as needing to run. The observers do not run immediately. They are queued and flushed on a microtask, so several writes in the same synchronous block coalesce into a single update pass.

A write marks observers and schedules a microtask flush; multiple writes coalesce into one pass

The default: microtask-coalesced

Writes happen synchronously into the underlying state object, but the re-runs (effect re-evaluation, DOM updates, computed re-evaluation) are deferred to a microtask. Multiple writes in the same synchronous block coalesce: by the time the microtask drains, only the final value of each path is visible, and each affected effect runs once.

// Three writes, one update pass.
this.count = 1
this.name = "x"
this.items.push(newItem)
// effects run when the microtask drains, after this block

This is the right default because it matches what users intuitively expect (set three things, render once) and it happens before the browser paints, so the user never sees an intermediate frame.

Batch: defer the flush across a block you control

The microtask already coalesces writes within one synchronous block. wildflower.batch(fn) (and the startBatch() / applyBatch() pair) extends that to a block you define explicitly, including across awaits, and is the mechanism the framework itself uses around compound operations that should be atomic from the rendering system's perspective, such as form submission and props propagation.

wildflower.batch(() => {
    this.count = 1
    this.name = "x"
    this.items.push(newItem)
})
// effects run here, once, after the batch closes

Synchronous flush: for interop

When non-reactive code needs to observe a state change immediately (test harnesses, certain animation libraries, headless rendering pipelines), flushSync() drains the pending effects right now instead of waiting for the microtask. It costs more because it cannot coalesce with later writes in the same block, so reach for it only at the interop boundary.

Internal fast paths: for the most common case, a single text binding whose value nothing else reads, the engine can write the new text straight to the DOM node and skip waking an effect at all. This is invisible to your code and never changes observable behavior; it is why a tight loop of single-field updates stays cheap.

4. How computed properties stay cheap

A computed property is a node in the same dependency graph. It tracks what it reads, exactly like an effect. Two properties keep computeds cheap, and they apply to every computed equally. There is no tiering, no promotion, and nothing to opt into.

A computed recomputes only when a source actually changed, and propagates only when its own result changed

It recomputes only when a source actually changed

Computed properties are lazy and validated on read. When something a computed read might have changed, the computed is flagged to check. On the next read, it confirms whether any of its sources truly changed value. If none did, it returns its cached result with no work. A computed downstream of a busy part of the graph is only re-evaluated when a value it actually depends on actually moves.

It propagates only when its own result changed

When a computed does re-evaluate, it compares the new result to the previous one. If they are equal, nothing downstream is woken. A recompute that lands on the same value stays contained and does not cascade into the effects and computeds that read it.

What this means in practice: deriving state with computed properties is close to free when inputs are stable, and chains of computeds do not amplify a change that does not actually alter a value. A computed like fullName() { return this.firstName + ' ' + this.lastName } re-runs only when one of those two fields changes, and only wakes the bindings reading fullName if the concatenated result is different.

5. Timing: microtask first, paint after

The framework's working assumption is to do as much as possible on the microtask, because microtasks run before the browser paints and have no animation-frame jitter. After a write, the effect flush, the binding and list updates, the computed re-evaluations, and the component render pass all run on the microtask, before the next paint.

Timeline: synchronous code, microtask drain (effects, render), then browser paint

Keeping the render pass on the microtask rather than requestAnimationFrame is deliberate: an interactive change commits in the same frame as the interaction, instead of waiting a frame for an animation callback. requestAnimationFrame is reserved for work that genuinely needs to align with paint or animation timing: the initial-mount bootstrap, transitions, and conditional reveals. Pools are the one part of the system that run on their own animation-frame loop, by design, because they exist for per-frame rendering.

If you see something update "a frame late": that is almost always a transition or a data-render reveal, which align to the animation frame on purpose. Ordinary data-bind, data-list, and computed updates land on the same frame as the interaction that triggered them.

6. Cross-entity reactivity

Components, stores, plugins, and pools all share the one dependency graph (section 1). Within an entity, dependency tracking is direct: reading a value inside an effect records a graph edge, and writing it wakes the observers. Reaching across entities, a component computed that reads a store's value, goes through a dedicated tracking surface so that the dependency is still recorded.

Cross-entity reads go through a tracking proxy that records the dependency; when the source entity changes, the framework re-evaluates the dependents that read it

The tracking surface is the proxy returned by wildflower.getStore(), wildflower.getComponent(), and the $entity-name accessor. When you read wildflower.getStore('cart').total from inside component A's computed, you are not reading the cart's raw state; you are reading through a tracking proxy that records on A's side that "this computed depends on the cart's total."

That recorded dependency is what drives the update. When the cart's state changes, the framework re-evaluates the dependents that read it, so A's computed recomputes and any bindings reading it update. The dependent does not poll, and it does not wire up its own callback by hand: the cross-entity dependency is registered once, when the read happens, and is torn down with the dependent when it is destroyed, so there is no manual subscribe or unsubscribe to manage.

// In component A
computed: {
    cartTotal() {
        // returns the cart's total, reactively
        return wildflower.getStore('cart').total
    }
}

// In the cart store, somewhere
this.items.push(item)  // changes the cart
// the framework re-evaluates A's cartTotal, which read the cart through getStore
The supported pattern: always reach across entities via getStore(), getComponent(), or $entity-name. If you grab a raw reference to another entity's state object directly (for example by capturing it in a closure), the tracking proxy is bypassed and the dependency is silently lost. The reactive update will not fire. The framework has no way to detect this; it relies on the convention that cross-entity reads always go through the tracking surface.

7. Lifecycle windows that change the rules

The reactive engine behaves slightly differently depending on where in a component's life the operation happens. Two windows are worth knowing.

Pre-init action queueing

If a DOM event (a click, an input event, a keydown) fires after a component's element exists but before its init() hook has finished, the action handler does not run immediately. It is queued. When init() returns, queued handlers replay in their original order. This matters most for components that subscribe to slow-loading stores: init may await a Promise.all of subscriptions for several macrotasks, and any user interaction during that window would otherwise hit a partially-initialized component.

Replayed handlers see the original DOM event, but with one limitation: event.preventDefault() is a no-op by replay time because the browser has already processed the default action. For forms that need to reliably block submission across the replay boundary, use data-event-prevent on the form element. The framework intercepts the event before user code runs.

One related constraint: a method named exactly init, beforeInit, destroy, beforeDestroy, onUpdate, beforeUpdate, onError, or tick is treated as a framework-driven lifecycle hook and is not queueable. Don't reuse those names for action handlers. The most common trap is tick: it gets called every animation frame for components in the pool loop, not on click.

Destroy-time cleanup

Effects are owned by a disposal scope that mirrors the component (and, for lists, each row). When a component is destroyed, the framework disposes its scope, and every effect and dependency edge created under it goes with it, in one pass. There is no registry to scan and no periodic garbage collection looking for orphaned effects; ownership is what makes teardown deterministic.

The user's destroy() hook fires before the scope is torn down, and it can safely mutate state: any effects those mutations would have woken are about to be disposed along with the scope, so nothing leaks past teardown.

8. Conditional reads and dependency tracking

The framework tracks dependencies by intercepting reads through the state proxy. When you read this.foo inside a computed or effect, the get trap records "this binding depends on foo." When that field later changes, every binding that read it is queued to re-run.

The constraint: only reads that actually execute get tracked. JavaScript's short-circuit semantics for &&, ||, and ternary ?: mean that some reads in the source code don't always happen at runtime. Consider:

computed: {
    isOpen(item) {
        return this.openField === 'status' && this.openId === item.id;
    }
}

When isOpen first evaluates with openField equal to null, the && short-circuits and this.openId is never read. The binding's tracked dependencies are { openField } only. Now imagine the user flow that opens a popover and then switches to a different row:

  1. Click row A. openField changes from null to 'status', openId changes from null to 'a'. Every binding that tracked openField wakes and re-evaluates. This time the && doesn't short-circuit (left side is truthy), so openId gets read and tracked. Every binding now has both fields as dependencies. UI updates correctly.
  2. Click row B. openField stays 'status'. Only openId changes (from 'a' to 'b'). Bindings that tracked both fields wake. But bindings whose initial evaluation had short-circuited at openField may have tracked only that one field, depending on render order. Those bindings don't wake. Their rows' DOM never updates. UI is wrong.

The symptom is non-deterministic across reloads: sometimes the framework happens to evaluate every row's binding under a state shape that reads both fields, sometimes it doesn't. Initial render order, click order, and which row was first to evaluate truthy all influence which bindings have complete dependency sets.

This is a property of all runtime-proxy reactive systems (Vue, Solid, MobX, Preact Signals). It is not a WildflowerJS bug; it is the price of "no compiler." The compiler-based alternative (Svelte, Vue's <script setup> with reactive transforms) extracts dependencies via AST analysis at build time and records them regardless of control flow. WildflowerJS trades compile-time analysis for the no-build-step authoring story, so this characteristic comes with the runtime-proxy family.

The fix is to read all potentially-relevant fields eagerly at the top of the computed, before any branching:

computed: {
    isOpen(item) {
        const f = this.openField;   // always read; always tracked
        const id = this.openId;     // always read; always tracked
        return f === 'status' && id === item.id;
    }
}

The eager reads force both proxy reads on every invocation, so both fields end up in the binding's dependency set from the first evaluation onward. Subsequent state changes to either field correctly wake the binding.

This pattern applies anywhere a computed or effect conditionally reads state: &&, ||, ternary, if/else, early return. The rule is mechanical: every field the computed could read on any branch should be read once before the branching begins.

9. When to think about any of this

The defaults (microtask coalescing, lazy computed validation, post-init action dispatch) are correct on their own. You don't need to know any of this to write code that works. The page exists for the cases where you want to intentionally step outside the defaults, and you need to understand the machinery in order to do that confidently.

Those cases are:

  • You are debugging a "why didn't this update?" symptom. The most common cause is a closure-captured reference to another entity's state, bypassing the tracking proxy that getStore(), getComponent(), and the $entity-name accessor would have provided. The second most common is a conditional read that never tracked a field (section 8 above). See Communication for the supported cross-entity patterns and Common Mistakes for the specific anti-patterns.
  • You are debugging a "why did this fire twice?" symptom. Look at whether a component is being re-initialized, or whether two writes you expected to coalesce actually happened in separate microtask turns (for example, separated by an await). Section 3 above describes how coalescing works and how batch() extends it across awaits.
  • You are writing a plugin or a custom directive. Plugins use the same engine as components, but your plugin's effects need to register under the right disposal scope or they will not be cleaned up at destroy. See Basic Plugins for the registration shape and Advanced Plugins for the lifecycle and effect-cleanup details.
  • You are doing animation-heavy or high-frequency work. Pools exist precisely because the per-component overhead would be prohibitive at hundreds or thousands of items updating per frame. A pool sets up one handle and one renderer regardless of how many items it holds, and runs on its own animation-frame loop. See Why Pools? for the motivating use cases, Pools for the API, and Entity Model for the per-entity declaration shape.
  • You are interoperating with non-reactive code. The batch API (wildflower.batch(fn) or startBatch()/applyBatch()), flushSync(), and wildflower.whenSettled() are the bridges into systems that cannot be retrofit to the microtask drain. The batch path is described in section 3 above; the timing model in section 5 above.

Outside those cases, the engine fades into the background. That is the design goal.

The single most important thing to remember: reads through the framework's tracking surfaces (your own this.foo properties, getStore(), getComponent(), $entity-name) participate in reactivity. Reads through anything else (closures over external references, manually captured objects) do not. When in doubt, route through the tracking surface.