Scaffold a new ViteFrame app — a lightweight SPA framework built on Vite with file-based routing, reactive state, and dynamic HTML components. No external JS dependencies.
npm create viteframe@latest my-app
cd my-app
npm install
npm run devOr with pnpm / yarn:
pnpm create viteframe my-app
yarn create viteframe my-appsrc/
├── core/
│ ├── router.js # File-based SPA router (~180 lines)
│ ├── state.js # Reactive state store (~100 lines)
│ └── components.js # Dynamic component loader (~120 lines)
├── pages/
│ ├── landing.page.html # → /
│ ├── about.page.html # → /about
│ ├── docs.page.html # → /docs
│ └── [id]post.page.html # → /post/:id
├── components/
│ ├── header.comp.html
│ └── toast.comp.html
├── styles/
│ └── main.css # Design tokens + utility classes
└── main.js # Bootstrap
All framework code lives in your project — read it, modify it, own it.
File name determines the route. Drop files in src/pages/.
| File | Route |
|---|---|
landing.page.html |
/ |
about.page.html |
/about |
[id]post.page.html |
/post/:id |
[id-slug]post.page.html |
/post/:id/:slug |
[lang-id]post.page.html |
/post/:lang/:id |
window.__vf.navigate("/about");
window.__vf.navigate("/post/42", { replace: true });Shared across all pages. Persists during navigation.
const { globalState } = window.__vf;
globalState.set("user", { name: "Alice" });
globalState.get("user");
globalState.subscribe("user", (val) => console.log(val));
// Bind value → DOM (auto-updates when state changes)
globalState.bind("user", el, (u) => u?.name ?? "Guest");
// Two-way bind: input ↔ state
globalState.bindInput("search", inputEl);
// Partial merge
globalState.merge("settings", { darkMode: true });Isolated to a single page visit. Created inside onMount, discarded on navigation.
export function onMount() {
const { createPageState } = window.__vf;
const state = createPageState({ count: 0, open: false });
state.update("count", (n) => n + 1);
state.bind("count", document.getElementById("counter"));
}const { computed, globalState } = window.__vf
const fullName = computed(
['firstName', 'lastName'],
[globalState, globalState],
(first, last) => `${first} ${last}`
)
fullName.get() // 'Alice Smith'
fullName.subscribe(v => …) // fires when either dep changesComponents are .comp.html files in src/components/. They support scoped <style> and <script> blocks.
<!-- Simple -->
<component src="header"></component>
<!-- With props via data-* attributes -->
<component src="card" data-title="Hello" data-body="World"></component><!-- src/components/card.comp.html -->
<div class="card">
<h3>{{title}}</h3>
<p>{{body}}</p>
</div>const { mountComponent } = window.__vf;
await mountComponent("#target", "card", { title: "Hello", body: "World" });Add <script data-lifecycle> to any page and export onMount:
<script data-lifecycle>
export function onMount({ params, query, globalState }) {
// params → route params e.g. { id: '42' }
// query → ?key=val e.g. { tab: 'info' }
// globalState → shared store
console.log("id:", params.id);
console.log("tab:", query.tab);
const { createPageState } = window.__vf;
const state = createPageState({ count: 0 });
}
</script>In src/main.js:
router.beforeEach(({ pathname }) => {
if (pathname.startsWith("/admin") && !globalState.get("user")) {
router.navigate("/login");
return false; // cancel navigation
}
});Include <component src="toast"> on a page, then anywhere:
window.toast("Saved!", "success"); // green
window.toast("Something broke", "error"); // red
window.toast("FYI", "info", 5000); // purple, 5sAll colors, spacing, and radii are CSS variables in src/styles/main.css.
/* Override in your own CSS */
:root {
--accent: #your-color;
--radius: 12px;
}Dark/light theme is toggled by setting data-theme="light" on <html>:
window.__vf.globalState.set("theme", "light");MIT