Vanilla JS SPA architecture. No build tools. No npm. ~3KB of dependencies.
Build real SPAs with just an HTML file and JavaScript. No webpack. No npm install. No build step.
Don't abstract what the browser already does well.
fetch, localStorage, history.pushState — browsers have refined these APIs for 20 years. Lite-SPA combines them with a tiny reactive state layer (~1.6KB) to produce something that punches well above its weight.
| Audience | Why Lite-SPA |
|---|---|
| Backend developers | Build admin panels without a frontend build pipeline |
| Indie hackers / solo devs | Start in seconds with CDN only |
| Legacy project maintainers | Drop jQuery without adopting React |
| Embedded web UI | IoT consoles, internal dashboards |
| AI Coding Agents | Extremely "Agent-Friendly" due to zero build steps and high code density |
In the era of AI-assisted coding (e.g., Cursor, Copilot, Claude Engineer), Lite-SPA provides an optimized environment for AI agents to read, write, and debug code:
- Zero Build Failures: No Webpack, Vite, or TypeScript compilation means AI agents won't waste API tokens fixing package manager resolution errors or bundler configs.
- Ultra-Compact Context: The entire app shell, routing, and state flow fit in a single folder. The agent can easily ingest the whole codebase at once, reducing hallucinations.
- Simple State Reasoning: Preact Signals use synchronous
.valueupdates. AI agents reason about this much better than React's asynchronous render cycle hook rules (e.g., stale closure issues, dependency arrays). - Pure Web Standards: Standard HTML templates and ESM are heavily represented in LLM training data, resulting in extremely clean and error-free code generation.
Tip
This repository includes a SKILL.md file. Feed it to your AI agent (or copy it into your .cursorrules/.clinerules) to immediately teach it the Lite-SPA design patterns and constraints.
| Concern | Tool | Size |
|---|---|---|
| Routing | page.js | 1.5KB |
| State | @preact/signals-core | 1.6KB |
| Styling | Tailwind CSS (CDN) | — |
| Pages | fetch + insertAdjacentHTML |
0KB |
Total external JS: ~3KB. Build steps: 0.
- Copy the
template/folder to your project directory. - Start a local static web server in that directory (required due to browser CORS policies blocking
fetchrequests on thefile://protocol).
You can run a local static server instantly using any of the following methods:
- VS Code: Install the Live Server extension, right-click
index.html, and select Open with Live Server. - Node.js: Run
npx serve -sin your project folder.[!IMPORTANT] When using
npx serve -s(SPA mode), you must disablecleanUrlsin aserve.jsonfile in your folder, otherwise it redirects.htmlpage template requests and causes recursive nesting. Createserve.jsonwith:{ "cleanUrls": false } - Python: Run
python -m http.server 8000in your project folder. - PHP: Run
php -S localhost:8000in your project folder.
template/
├── index.html ← App shell
├── app.js ← Routing + logic
├── store.js ← Global state (signals)
├── i18n.js ← Translation dictionary
└── pages/
├── home.html
└── about.html
No
npm install. No config files (unless usingnpx serve). Just run a simple static server and code.
When deploying a Single Page Application (SPA) to a hosting platform, directly accessing or refreshing a virtual route (e.g., /dashboard) will result in a 404 error because the file does not physically exist on the server. You must configure rewrite rules to redirect all virtual routes to index.html while allowing static assets (JS, CSS, HTML templates, images, etc.) to load normally.
In the AWS Amplify Console, navigate to Rewrites and redirects under your app settings and add the following rule:
[
{
"source": "</^[^.]+$|\\.(?!(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json|webmanifest|html)$)([^.]+$)/>",
"target": "/index.html",
"status": "200"
}
]Create a vercel.json file in the root of your project to disable cleanUrls (which prevents automatic .html extension stripping that breaks dynamic template fetching) and rewrite virtual routes to index.html:
{
"cleanUrls": false,
"rewrites": [
{
"source": "/((?!.*\\.(css|gif|ico|jpg|js|png|txt|svg|woff|woff2|ttf|map|json|webmanifest|html)$).*)",
"destination": "/index.html"
}
]
}| Example | What it demonstrates | Live |
|---|---|---|
| 01-counter | Signal basics, shared state across pages | Open |
| 02-todo | Complex state using Signals, state persistence (localStorage) | Open |
| 03-memo (Tutorial) | Step-by-step beginner's tutorial: custom routing, signals, inputs | Open |
Tip
If you are new to Lite-SPA, check out the 03-memo Step-by-Step Tutorial (or the Korean version: README.ko.md). It explains how to build a new page and manage state from scratch!
const count = signal(0);
// Runs automatically whenever count changes
effect(() => {
document.getElementById('display').textContent = count.value;
});
// Trigger the effect
count.value++;async function renderPage(pageId) {
// Fetch HTML only on first visit
if (!loadedPages.has(pageId)) {
const html = await fetch(`pages/${pageId}.html`).then(r => r.text());
document.getElementById('app-content').insertAdjacentHTML('beforeend', html);
loadedPages.add(pageId);
}
document.querySelectorAll('.page-view').forEach(v => v.classList.add('hidden'));
document.getElementById(`page-${pageId}`)?.classList.remove('hidden');
}MIT © 2026