I've started to think something like petite-vue and twind would let you build small internal tools quickly, there are some major downsides to it which is why I haven't committed yet.
Why I built it Modern stacks are powerful but often heavy: bundlers, compile steps, framework DSLs, local CLIs. For internal tools, small apps, and edge/serverless deployments, I wanted something you can view-source, paste into a page, and ship.
What it is:
Runtime-only: no build or VDOM compile; hydrate behaviors directly on HTML. HTML directives: e.g. +click, lifecycle +load / +loaded / +unload / +unloaded. Zero APIs: dagger.js works in pure declarative mode, modules and directives provide everything you need to build your application. Web-Components-first: works alongside Custom Elements; keep concerns local. Distributed modules: load small, focused script modules via CDN. Progressive enhancement: the page renders without a build step.
Use cases:
Admin panels & dashboards that don’t warrant a full toolchain Embed widgets, docs-sites with interactive bits Edge/serverless apps where cold start and simplicity matter
Links
GitHub: https://github.com/dagger8224/dagger.js Docs/Guide: https://daggerjs.org Examples: https://codepen.io/dagger8224/pens
I’d love feedback on edge-cases, and where it breaks. Happy to answer tough questions here.
I've started to think something like petite-vue and twind would let you build small internal tools quickly, there are some major downsides to it which is why I haven't committed yet.
I started out developing UIs using petite-vue, unfortunately ended up rewriting it to use full Vue.js after running into too many of its limitations and issues, which given that it's abandon-ware wont ever be resolved. As such I'd strongly recommend against starting projects with PetiteVue and just use the ESM build of Vue.js which also doesn't require a build step, has the full feature-set, is actively used and maintained, etc.
Either way developing Web Apps (using Vue) without a build step using JS Modules is now my preference which I've written about in my Simple, Modern JavaScript post [1]
dagger.js aims a bit differently: it’s even smaller in scope (no virtual DOM, no reactivity system, no SFCs), and is designed to pair directly with Web Components and plain HTML snippets. The trade-off is fewer features, but also less surface area and almost zero “lock-in” — you can literally view-source and drop it into a page.
So I see them as different tiers on the same spectrum: Vue (ESM) if you want a mature, batteries-included framework without a bundler; Dagger.js if you just want minimal runtime directives and WC interop with almost no moving parts.
Appreciate you sharing the blog link — I’ll check it out. Always happy to see more people pushing the “modern JS without a toolchain” approach.
dagger.js sits in the same no-build space, but deliberately strips it down even further: no VDOM, no reactive system, no SFCs. Just HTML with attributes like +click / +load, and it plays nicely with native Web Components. The trade-off is fewer features, but also less surface area and almost nothing to configure.
So if Vue ESM is “full-featured without the tooling overhead,” dagger.js is more like “minimal glue you can drop in via <script> when you want to stay as close to plain HTML/JS as possible.”
You should be able to drop in code dynamically on any existing website with JS Modules, I show an example of this in one of our release notes to showcase the versatility of JS modules [1], which:
- Dynamically adds an Import Map with references to Vue, an external lib + Vue component library
- Dynamically creates and mounts a new Vue Component on the fly
- Drops in a fully functional and editable Data Grid with API bound forms, API + form validation, etc
Basically you should be able to do most things on a deployed page as can be done in your "no-build" ESM Web App.I did try building web components with Lit [2], unfortunately WC's encapsulation and shadow dom made it difficult to enable theming and maintain shared global tailwind styles, so ditched it and went back to Vue. Although it's a good option for creating encapsulated components that don't need to share styles.
[1] https://docs.servicestack.net/releases/v6_06#js-modules-quic...
[2] https://lit.dev
like vue, by default aurelia uses a build step, but serving it directly from a CDN or your own server is possible. i am actually working on a site that does that right now.
one thing i like about aurelia is that a template and js code are associated by name, so <this-view></this-view> translates to this-view.js: class ThisView {}, this-view.html, this-view.css, so they all form one unit, and i only need to import the js and specify the class name to load and have everything else defined automatically.
if i read https://daggerjs.org/#/module/introduction correctly, then you treat each of those as independent modules, that need to be specified separately.
You’re right that dagger.js takes a different approach: it treats HTML, JS, and CSS as independent modules that you explicitly wire together. The reason is to keep everything buildless and decoupled — you can serve each piece directly from a CDN, mix and match across projects, and avoid any assumptions about file structure or bundling.
The trade-off is exactly as you noticed: dagger.js doesn’t auto-bind a .js class to a .html and .css file. It gives up that convenience in exchange for transparency and flexibility in how modules are loaded.
That said, I think your idea of a convention layer on top of dagger.js (similar to Aurelia’s “unit” model) is interesting — it could make sense as an optional helper for people who want stronger coupling between files.
If you give it a try, I’d love to hear how it works out for you. Feedback from people who prefer “as little JS as possible” is especially valuable.
aurelia doesn't need a build step to wire things together. nor does it remove transparency or flexibility. because you can still specify all parts explicitly if you want to.
it should not be hard to create a function that does the same for dagger.js:
a configuration like this:
{
"remote_view_module": {
"uri": "./thispage.html",
"type": "view"
},
"remote_style_module": {
"uri": "./thispage.css",
"type": "style"
},
"remote_script_module": {
"uri": "./thispage.js",
"type": "script"
},
"remote_json_module": {
"uri": "./thispage.json",
"type": "json"
}
}
can easily be generated with a function: function load_modules(name) {
return {
"remote_view_module": {
"uri": "./"+name+".html",
"type": "view"
},
"remote_style_module": {
"uri": "./"+name+".css",
"type": "style"
},
"remote_script_module": {
"uri": "./"+name+".js",
"type": "script"
},
"remote_json_module": {
"uri": "./"+name+".json",
"type": "json"
}
}
}
thus reducing 18 lines to one: load_modules("thispage")
anyone who needs the flexibility can still specify the parts they want to wire together manually. this is just about making the default easier.you could even allow some names to be specified and still reduce the code a user needs to write:
load_modules({ name: 'thispage', style: 'other.css', json: undefined })
that would use 'thispage' as the default name, but override the style and remove json.