I've been building a JavaScript framework called Voodoo.js, and I'd rather get torn apart in the comments than hear that it looks cool.
It started with one question:
How much JavaScript do we actually need to write for everyday interactive interfaces?
For a lot of apps I don't want to build a component tree, configure a bundler, install five libraries, wrap the API, manage state somewhere else, and then wire it all back into HTML.
Sometimes I just want to keep writing HTML.
GitHub: https://github.com/kwy404/Voodoo.js
The 30-second version
A reactive counter:
<div v-data="{ count: 0 }">
<button @click="count--">-</button>
<strong>{ count }</strong>
<button @click="count++">+</button>
</div>
No querySelector. No addEventListener. No innerHTML. No Virtual DOM.
Voodoo observes the real DOM and connects HTML directives to a fine-grained reactive system built on Proxy.
"Why another JS framework?"
Fair question. It's the first one I'd ask too.
Voodoo isn't trying to replace React, Vue or Svelte. The target is different: projects where the server already renders the HTML.
Laravel, Django, Rails, PHP, ASP.NET, Node, Go templates — plus small frontends where a full SPA is overkill.
Philosophically it lives somewhere near Alpine.js, HTMX and petite-vue. The difference I was chasing: HTTP, forms, validation, state, components, routing, UI helpers and reactivity should feel like one system, not five glued together.
Declarative HTTP
This is the part I enjoy using most.
Instead of wiring a button by hand:
button.addEventListener('click', async () => {
if (!confirm('Delete user?')) return
await fetch('/api/users/42', { method: 'DELETE' })
showToast('User deleted')
})
You declare the intent:
<button
v-delete="/api/users/42"
v-confirm="Delete this user?"
v-toast-success="User deleted">
Delete
</button>
The goal isn't to make JavaScript disappear. It's to stop writing JavaScript that only restates what the HTML already says.
Forms
Same idea:
<form
v-submit="/api/users"
v-validate
v-toast-success="User saved">
<input name="email" type="email" required>
<button type="submit">Save</button>
</form>
For most CRUD screens, that's the whole thing. When you need more, plain JavaScript is still right there. Voodoo is meant to be progressive, not restrictive.
No eval(), no new Function()
Take this:
<button @click="count++">
The lazy implementation is new Function(expression) or eval(expression). Voodoo does neither.
There's a full expression engine inside:
Lexer → Pratt Parser → AST → Interpreter
Expressions are parsed and interpreted by Voodoo itself. That gives the framework control over which syntax is supported and what the expression environment can touch — and honestly it's the most interesting engineering in the project.
Fine-grained reactivity
The reactive layer uses Proxy, tracking dependencies per object and per property:
reactive object
↓ property accessed
dependency tracked
↓ property changed
only dependent effects run
No Virtual DOM diff between updates. The real DOM is patched directly.
The reactivity package exposes a familiar surface:
reactive() ref() computed()
watch() watchEffect() effectScope()
toRaw() markRaw()
If you've used a modern reactive framework, none of this should surprise you.
Works with or without a build step
Drop it in:
<script src="voodoo.js"></script>
<div v-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div v-show="open">Hello DEV Community 👋</div>
</div>
Or import it like anything else:
import { reactive } from 'voodoojs/reactivity'
Both audiences matter to me: the "paste a script tag" crowd and the "give me proper ESM" crowd.
Where it fits
Server-rendered HTML + Voodoo.js → Reactive interface
A Laravel view, for example:
@foreach($users as $user)
<button
v-delete="/users/{{ $user->id }}"
v-confirm="Delete user?">
Delete
</button>
@endforeach
The backend keeps rendering the app. Voodoo adds the interactive layer. No separate frontend application.
The honest part
What began as an experiment grew into a large framework: reactivity, directives, components, stores, HTTP, forms, validation, persistence, routing, i18n, UI utilities, animation, drag and drop, charts, streaming, devtools, and a CLI.
That growth is now the problem. The project doesn't need another hundred features — it needs stability.
Current focus:
- 🔒 Security hardening
- 🧩 Parser edge cases
- ✅ Test coverage
- 📐 API stability
- 📦 Bundle modularity
- ⚡ Performance benchmarks
- 📚 Documentation
- 🚀 Package and release quality
I'd rather ship a small set of features people can trust in production than a huge one nobody can.
What I'm asking for
If you work with Alpine.js, HTMX, Vue, petite-vue, Stimulus, Livewire, server-rendered apps, or plain JS — I want your read on this:
- What would stop you from using something like Voodoo.js?
- Which parts of the API feel intuitive?
- Which parts feel unnecessary?
- What would you need to see before calling it production-ready?
And if you like working on JS internals, there's real work available: parsers, reactivity, testing, browser compatibility, TypeScript, performance, security, docs, components.
GitHub: https://github.com/kwy404/Voodoo.js
If you find something questionable in the architecture, the API, the security model or the implementation, tell me. That's the feedback I actually need.
Top comments (6)
The shift from “adding capabilities” to proving stability is probably the most important stage for a framework like this. Once reactivity, HTTP, forms, routing, parsing, and other pieces start living under one abstraction, the challenge becomes making their interactions predictable rather than adding more features. I’d be especially interested in seeing failure cases and compatibility boundaries documented alongside the happy-path API, because that’s usually where a new framework earns production trust.
This matches where the project actually is, and your framing turned out to be diagnostic rather than just directional.
Every serious bug found in the last cycle came from interactions, not from any single module. A component registered before the walker reached its ancestor bound to the wrong scope, permanently. v-bind ran after v-model, so :min and :step landed after the value and the browser silently rounded 0.12 to 0. The devtools inspector was blind whenever attribute cleanup was on, which is the default. Each piece was correct in isolation and wrong in combination.
The sharpest signal was a getter-flattening defect found and fixed three separate times, in three modules, by three different people. That is not three bugs. It is one rule nobody wrote down, so each module reinvented it and three got it wrong. The fix was a written convention plus a test that fails when a public API object freezes a getter, so the fourth occurrence breaks CI instead of shipping.
On failure cases you are right that we are thin. We have them where security forced the issue: why the parser refuses eval, why HTTP retry will not repeat a POST without an idempotency key, why a "private" room is the server's responsibility and not something a client can promise. But the general shape of the docs is still happy path, and closing that is next.
Two things from the same period, both from measurement rather than intuition. A CPU profile showed a teardown hotspot was reading a live childNodes collection that the DOM rebuilds on every access; switching to sibling traversal cut large-list teardown by about a third. And a parser fuzzer found three genuine bugs a hand-written suite would never reach, including one that silently accepted "\uZZZZ" as \0.
Where it honestly stands: the project scores itself 5.4 out of 10. Correctness is at 10, but there are no browser tests, docs coverage is weak, and accessibility has open items. Publishing that number is more useful than a badge claiming everything is fine.
These benchmarks are actually pretty encouraging for Voodoo.js considering how early the project still is.
The benchmark measures the median execution time over 30 samples with production-minified builds — lower is better.
Voodoo.js results:
Create 1,000 rows: 183.01 ms
Update every 10th row: 12.21 ms
Clear 1,000 rows: 30.77 ms
Creating large batches of DOM nodes is currently the main area where Voodoo.js needs optimization. That's not something we're trying to hide — it's exactly the kind of benchmark that helps identify where the runtime can improve.
What's interesting, though, is the update performance. Voodoo.js already performs better than Vue (14.66 ms) and significantly better than Alpine (94.03 ms) in this test, despite being a very young project.
Clearing 1,000 rows is also already in the same general range as React, Vue and Preact.
Voodoo.js is not being built just to win synthetic benchmarks. The goal is to explore a simple, expressive framework architecture while keeping the runtime small and increasingly fast.
And the important part is: there's still a lot of low-hanging fruit for optimization.
This is the baseline, not the finish line.
GitHub: github.com/kwy404/Voodoo.js
If you're interested in framework internals, performance optimization, or just want to experiment with a different approach to reactive UI development, contributions and feedback are very welcome.
Voodoo.js is already competitive in some update scenarios. Now the fun part begins: making it fast everywhere.
Good idea !!
Thank you for checking out this project.If you found this repository helpful or learned something new, please consider leaving a star. Your support increases the project's visibility and motivates me to keep building and improving it.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.