Getting Started
Try it online
Open the full Quiver starter in your browser — no installation needed.
Prerequisites
- Bun 1.4+ — the supported package manager and runtime
No Bun? npm install && npm run dev also works on Node.js 22.12+ (required by Vitest 5); this is how the StackBlitz button runs the project. bun.lock is the only lockfile, so npm resolves fresh versions.
Browser support
Quiver's router is built on the Navigation API, so the apps you build with it run in modern browsers only:
| Browser | Minimum version |
|---|---|
| Chrome | 102 |
| Edge | 102 |
| Safari | 26.2 |
| Firefox | 147 |
Installation
Scaffold a fresh project without Quiver's git history:
bunx degit go4cas/quiver my-app
cd my-app
bun installOr clone the repository directly (keeps the full history), or click Use this template on GitHub:
git clone https://github.com/go4cas/quiver
cd quiver
bun installRunning the app
bun run devOpen http://localhost:5173. The app hot-reloads on every save.
Available scripts
| Command | Description |
|---|---|
bun run dev | Start the Vite dev server |
bun run build | Production build to dist/ |
bun run preview | Preview the production build locally |
bun run test | Run unit tests once (Vitest) |
bun run test:watch | Run unit tests in watch mode |
bun run test:e2e | Run end-to-end tests (Playwright) |
bun run typecheck | Type-check src/ via JSDoc annotations (tsc checkJs, no build step) |
bun run docs:dev | Start the documentation site locally |
bun run docs:build | Build the documentation site |
bun run docs:preview | Preview the built documentation site locally |
Folder structure
public/ # Served at root URL as-is — favicon, OG images, robots.txt
src/
├── assets/ # Images, SVGs, fonts — imported in components, hashed on build
├── framework/ # Router, DI, store, app bootstrap — framework internals
├── pages/ # File-based routes
├── layouts/ # Page wrapper components
├── components/ # Reusable UI components
├── state/ # Global reactive state modules
├── composables/ # Reusable logic functions
├── utils/ # Pure helper functions
└── main.js # App entry point
tests/
├── framework/ # Unit tests for framework utilities (Vitest)
├── composables/ # Unit tests for composables (Vitest)
└── e2e/ # End-to-end tests (Playwright)
docs/
└── guide/ # Developer guidesStatic assets
Vite handles two kinds of static assets out of the box.
public/ — files served at the root URL without any processing. Use this for assets that need a stable, predictable path: favicons, Open Graph images, robots.txt. Reference them with an absolute path:
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />src/assets/ — assets imported directly in JavaScript. Vite resolves the import to a content-hashed URL and copies the file to dist/ at build time:
import avatarUrl from '../assets/avatar.svg'
html`<img src="${avatarUrl}" alt="Avatar" />`Use src/assets/ for any image, SVG, or font that is referenced inside a component.
What you own
These directories are yours — add, edit, and delete files here freely.
| Directory | What to put here |
|---|---|
public/ | Assets with stable URLs — favicons, OG images, robots.txt |
src/assets/ | Images, SVGs, fonts imported inside components |
src/pages/ | Route pages — one file per route |
src/components/ | Reusable UI components |
src/layouts/ | Page layout wrappers |
src/state/ | Global reactive state modules |
src/composables/ | Reusable logic functions |
src/utils/ | Pure helper functions |
tests/e2e/ | End-to-end tests for your features |
tests/framework/ | Unit tests for any pure utilities you write |
tests/composables/ | Unit tests for composables you write |
What to leave as-is
These are framework internals — no need to touch them for normal development.
| File / Directory | Why |
|---|---|
src/framework/ | Router, DI context, store wrapper, and app bootstrap. Modify only if extending the framework itself. |
src/main.js | Entry point. Only change it to add a top-level provide() call or a plugin. |
vitest.config.js | Leave unless changing which test directories are scanned. |
playwright.config.js | Leave unless changing test directories or the dev server port. |
vite.config.js | Leave unless adding Vite plugins. |
App bootstrap
src/main.js registers DI values, starts the router, then mounts the app:
provide('app', { name: 'Quiver', tagline: 'The Demo Hub' })
await initRouter() // from './framework/router.js'
createApp() // from './framework/index.js'createApp() renders the current route (inside its layout) into the #app element from index.html, and throws if it is missing. Await initRouter() first, and run any other setup code (analytics, error reporting) before calling it.
Ready to build something? See the feature workflow guide.
One runtime dependency
Quiver's only runtime dependency is @arrow-js/core: reactive(), html, component(), watch(), onCleanup(), nextTick(). src/framework/app.js mounts the root template by calling it with the #app element — no separate render package.
Arrow.js rules to know
Two constraints come up often enough to call out before you start writing components.
Every reactive ${} slot must be a function
Arrow.js tracks dependencies lazily. A bare value is read once and never updated:
// Static — renders once, never updates
${userState.users.length}
// Reactive — re-evaluates whenever userState.users changes
${() => userState.users.length}Never put HTML comments inside templates
Arrow.js uses HTML comment nodes internally to mark ${} slot positions. Adding your own <!-- --> comments inside an html\...`template collides with this mechanism and throwsUncaught Error: Invalid HTML position`.
// Will break
html`
<!-- Section header -->
<h1>${() => title}</h1>
`
// Fine — use JS comments outside the template literal instead
// Section header
html`<h1>${() => title}</h1>`