Skip to content

Getting Started ​

Try it online

Open the full Quiver starter in your browser — no installation needed.

Open in StackBlitz →

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:

BrowserMinimum version
Chrome102
Edge102
Safari26.2
Firefox147

Installation ​

Scaffold a fresh project without Quiver's git history:

bash
bunx degit go4cas/quiver my-app
cd my-app
bun install

Or clone the repository directly (keeps the full history), or click Use this template on GitHub:

bash
git clone https://github.com/go4cas/quiver
cd quiver
bun install

Running the app ​

bash
bun run dev

Open http://localhost:5173. The app hot-reloads on every save.

Available scripts ​

CommandDescription
bun run devStart the Vite dev server
bun run buildProduction build to dist/
bun run previewPreview the production build locally
bun run testRun unit tests once (Vitest)
bun run test:watchRun unit tests in watch mode
bun run test:e2eRun end-to-end tests (Playwright)
bun run typecheckType-check src/ via JSDoc annotations (tsc checkJs, no build step)
bun run docs:devStart the documentation site locally
bun run docs:buildBuild the documentation site
bun run docs:previewPreview 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 guides

Static 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:

html
<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:

js
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.

DirectoryWhat 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 / DirectoryWhy
src/framework/Router, DI context, store wrapper, and app bootstrap. Modify only if extending the framework itself.
src/main.jsEntry point. Only change it to add a top-level provide() call or a plugin.
vitest.config.jsLeave unless changing which test directories are scanned.
playwright.config.jsLeave unless changing test directories or the dev server port.
vite.config.jsLeave unless adding Vite plugins.

App bootstrap ​

src/main.js registers DI values, starts the router, then mounts the app:

js
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:

js
// 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`.

js
// Will break
html`
  <!-- Section header -->
  <h1>${() => title}</h1>
`

// Fine — use JS comments outside the template literal instead
// Section header
html`<h1>${() => title}</h1>`