A small, zero-dependency JavaScript framework for building Single Page Applications with plain web standards.
VanillaForge gives you components, client-side routing, and an event bus in a
few small ES modules — no dependencies and no required build step. It runs
straight from src/ in the browser; the build is only for producing an
optimized bundle.
- Zero runtime dependencies — ships as plain ES modules.
- Small — the core is ~14.5 KB min+gzip (~48 KB minified).
- Batteries-included — icons, CSS/theming, alerts, self-hosted fonts, a shared reactive store, and an HTTP fetch wrapper, all built in. No Font Awesome, Bootstrap, SweetAlert, or Google Fonts required — but you can still bring them in if you want.
- Reactive signals —
this.signal(value)for fine-grained reactivity;computed(fn, deps)for derived values that update automatically. - Component composition — embed child components directly inside parent templates. Each child has isolated state, props, lifecycle, and event handling.
- Plugin system — every subsystem (icons, theme, alerts, fonts, store, http) is a plugin you can install, replace, or skip.
- Efficient updates — re-renders are applied with a tiny DOM-morphing diff, so only changed nodes are touched and focused inputs keep their cursor (see How rendering works).
- Client-side routing — history API, route params (
/users/:id), and a configurable fallback route. - Declarative events — wire DOM events to methods with
data-*attributes; the framework handles delegation and cleanup.
npx create-vanillaforge my-app
cd my-app
npm install
npm run devThe CLI prompts for a template. Pass --template=<name> to skip the prompt:
npx create-vanillaforge my-app --template=minimal # no plugins
npx create-vanillaforge my-app --template=full # all plugins
npx create-vanillaforge my-app --template=todo-app
npx create-vanillaforge my-app --template=router-appSee create-vanillaforge/README.md for the full CLI reference.
npm install vanillaforgegit clone https://github.com/Steve-GitCodex/vanillaforge.git
cd vanillaforge
npm install
npm run dev # build to dist/ and serve
npm run example # Todo app (no build needed)
npm run example:router # Routing + params demoYour first component:
import { createApp, BaseComponent, iconsPlugin } from './src/framework.js';
class HelloWorld extends BaseComponent {
constructor(eventBus, props = {}) {
super(eventBus, props);
this.name = 'hello-world';
this.state = { count: 0 };
}
getTemplate() {
return `
<div class="hello">
<h1>Hello, VanillaForge!</h1>
<p>Clicked ${this.state.count} times.</p>
<button data-action="inc">
${this.icon('plus', { size: 16 })} Click me
</button>
</div>
`;
}
getMethods() {
return { inc: () => this.setState({ count: this.state.count + 1 }) };
}
}
const app = createApp({ debug: true });
app.use(iconsPlugin); // built-in SVG icons
await app.initialize({ routes: { '/': HelloWorld } });
await app.start();The page needs a mount element (default id main-content, configurable via
createApp({ mountId })):
<div id="main-content"></div>
<script type="module" src="./app.js"></script>Calling setState() re-runs your getTemplate() and morphs the result onto
the live DOM instead of replacing innerHTML. The morph:
- patches only attributes/text/nodes that actually changed;
- preserves the focus and caret/selection of a focused input, so typing is never interrupted by a re-render;
- reconciles lists by
data-key, so reordering or removing an item reuses the existing DOM nodes instead of rebuilding the list.
getTemplate() {
return `<ul>${this.state.items
.map((it) => `<li data-key="${it.id}">${it.label}</li>`)
.join('')}</ul>`;
}Note: a full re-render still re-runs the whole template (then diffs it). Moving to fine-grained, signal-based updates is on the roadmap.
Bind DOM events to getMethods() entries with attributes. Each attribute maps to
exactly one event so a handler fires once:
| Attribute | Fires on | Typical use |
|---|---|---|
data-action |
click |
buttons, links |
data-change |
change |
checkboxes, radios, selects |
data-input |
input |
text inputs, textareas |
data-keydown |
keydown |
keyboard shortcuts |
data-submit |
submit |
forms |
Handlers receive (event, matchedElement). Listeners are delegated to the
component's root element once and cleaned up automatically on destroy.
- Todo App — local state, filtering, keyed list, and
focus-preserving input. Run with
npm run example. - Routing demo — a list view and a
/users/:iddetail view driven by route params. Run withnpm run example:router.
Tests run on Vitest with happy-dom (dev dependencies only — the framework itself stays dependency-free):
npm testCoverage includes the DOM morph (focus/selection preservation, keyed lists), the component lifecycle and event delegation, the router, the event bus, and both examples.
npm run build # bundle src/app.js + CSS into dist/
NODE_ENV=production npm run build # minified bundleThe build uses esbuild to bundle and tree-shake, and copies/minifies discovered CSS. See docs/build-system.md.
- Components Guide
- Component Composition
- Signals & Computed
- Plugin System & Built-in Icons
- HTTP Plugin
- Routing System
- Event Bus
- CLI Reference
- API Reference
- Build System
- GitHub Pages
- Roadmap
For maintainers returning after time away: see DEVELOPMENT.md.
Templates return plain HTML strings — escape all user-supplied values before interpolating them. VanillaForge ships a dedicated helper:
import { escapeHtml, html } from 'vanillaforge'; // or from './src/framework.js'
// Escape individual values
getTemplate() {
return `<h1>${escapeHtml(this.state.title)}</h1>`;
}
// Or use the html tagged template — every interpolation is auto-escaped
getTemplate() {
return html`<h1>${this.state.title}</h1>`;
}this.icon() and this.child() already return safe RawHtml — no extra
escaping needed. See Components — Escaping and XSS.
Content Security Policy: the theme, alerts, and fonts plugins inject
<style> elements at runtime, so your CSP requires style-src 'unsafe-inline'
(or a nonce/hash approach). Stricter CSP support is on the roadmap.
Modern browsers with ES2020+ support: Chrome 80+, Firefox 72+, Safari 14+, Edge 80+.
See docs/roadmap.md for the full list of completed and planned features.
Recent additions: HTTP plugin, computed signals, navigation guard docs, and CLI add subcommands.
Full release history in CHANGELOG.md.
Issues and pull requests are welcome. Please run npm test and npm run lint
before opening a PR.
MIT — see LICENSE.