No description
  • TypeScript 62.5%
  • Rust 30.7%
  • CSS 4.1%
  • JavaScript 1.3%
  • HTML 0.9%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Code Villain 0fdc4516f5 docs previews wake up: scoped CSS fixed, missing wiring added, sidebar sorted
@scope blocks prefixed with the view's own tag can never match — the
scoping root already scopes the rules — so seven previews styled
blank (drawer, otp-field, scroll-area, slider, tooltip, toggle-group,
toolbar). Previews that promised interaction now mirror their
components: slider drag and keys, drawer open/Esc/backdrop, tooltip
hover delay and sticky pointer, toolbar roving tabindex, checkbox
form behaviour, otp-field typing and paste, toggle-group presses.
The button page grows a filled primary and a disabled rule that
matches the static preview. Sidebar and catalog lost their Drawer
triplicate and read alphabetically. Dialog and alert-dialog demos
reworked; the radio preview refuses items without a value.
2026-08-24 14:57:57 +02:00
cli tooltip joins the registry: hover, focus, and CSS anchor positioning 2026-08-24 08:55:31 +02:00
docs docs previews wake up: scoped CSS fixed, missing wiring added, sidebar sorted 2026-08-24 14:57:57 +02:00
examples add alert-dialog, extract core activation, sync the tree 2026-08-23 21:14:22 +02:00
framework refactor examples and vendor polyfills 2026-08-23 09:55:02 +02:00
library tooltip joins the registry: hover, focus, and CSS anchor positioning 2026-08-24 08:55:31 +02:00
spec tooltip joins the registry: hover, focus, and CSS anchor positioning 2026-08-24 08:55:31 +02:00
.gitignore visual regression check for the docs site 2026-08-23 22:34:20 +02:00
AGENTS.md visual regression check for the docs site 2026-08-23 22:34:20 +02:00
CLAUDE.md docs: write down the contracts, and an entry point for whoever arrives next 2026-08-21 20:01:02 +02:00
LICENSE fix(templates): stop depending on very recent DOM lib definitions 2026-08-19 10:29:36 +02:00
Makefile visual regression check for the docs site 2026-08-23 22:34:20 +02:00
README.md add alert-dialog, extract core activation, sync the tree 2026-08-23 21:14:22 +02:00

vweb

A Rust CLI that scaffolds and maintains zero-dependency web-standards SPAs — projects whose entire runtime is native browser API. No framework, no router library, no state library, no CSS framework. The only tooling is Vite+ and TypeScript, both dev-only.

📖 Documentation: docs/index.html — hand-written static HTML, no build step. Open it in a browser, or serve docs/ anywhere.

cargo install --path cli

vweb new my-app          # scaffold
cd my-app
vweb dev                 # → vp dev

vweb g page about --nav About
vweb g component card --light
vweb g store cart --persist
make install
make check
make test
make build
make fmt
make docs-serve

Working on vweb itself? AGENTS.md is the orientation, and spec/ is what the project guarantees.

Commands

Command What it does
vweb new <name> Scaffold a project. --template framework|<path>|<git-url>
vweb dev / build / preview / test / fmt / check Proxy straight to vp, args forwarded verbatim
vweb g component <name> Custom element as .ts + .html + .css. --light for light DOM + @scope, --test for a test file
vweb g page <name> View (three files) and its route entry. --path /docs/:slug, --nav <label>
vweb g store <name> Typed store slice. --persist wires localStorage
vweb g test <path> Test file next to an existing module
vweb destroy <kind> <name> Reverses a generate — file plus route registration
vweb add <name>... Copy a headless component into src/ui. --list to see them, --force to overwrite
vweb remove <name> Take one back out, pruning core modules nothing else needs

generate is aliased to g, destroy to d.

Full reference: command map · vweb new · vweb generate · vweb destroy · headless components · vweb.toml and passthrough.

The component library

generate writes a blank skeleton; add writes a solved one. The components in library/ are headless — behaviour, ARIA and keyboard handling, and no CSS or markup at all (a test fails if a .css or .html file appears in there). State comes back out as data-* attributes, and that is the whole styling contract.

vweb add --list
vweb add collapsible dialog tabs switch

They are copied, not installed, because a project with zero runtime dependencies cannot install a component library — and because source you own beats a version you upgrade past. Each component declares the shared modules it needs in a manifest.toml; only those are copied, and vweb remove prunes the ones nothing is left asking for.

Full reference: headless components.

The framework

framework/ is what vweb new copies out: router, store, base component, design system, home page and 404. Nothing to delete before you start. It is embedded in the binary, so scaffolding works offline and the framework version always matches the CLI version.

--template names it framework, but that is the default, so you only ever pass it to be explicit. It also accepts a local directory or a git URL. Note that a git template is someone else's code: vweb prints the resolved source and refuses to run an install against it unless you pass --install.

How generation works

There is no TypeScript parser and no CSS surgery. Generators write whole files, and where something must be registered they splice text around a marker comment. A project contains exactly one marker:

// src/router/routes.ts
export const routes: RouteDefinition[] = [
  // @vweb:routes
]

Component stylesheets declare their own @layer and @scope, so nothing central registers them, and generated route entries carry a // @vweb:route:<slug> tag that makes vweb destroy exact — an integration test asserts routes.ts comes back byte-identical.

The long version: the generation model and why destroy is exact.

Building apps with it

A scaffolded project is documented in two places: README.md inside the project (it ships with the template, so it works offline) and the runtime half of the docs site — app anatomy, components, state, routing, styling, platform APIs, and gotchas and deployment.

Editing the framework

framework/ is a source for the CLI, not a runnable project — it has no node_modules, so an editor opened on it type-checks with whatever TypeScript it bundles rather than the pinned 7.x, and reports vitest and vite-plus as unresolved. Both are expected there and neither appears in a scaffolded project.

Because of that, framework source avoids depending on very recent DOM library definitions. src/env.d.ts types the slice of the Navigation API the router uses, and vendor/polyfills/invokers.ts types requestClose locally. Neither is a polyfill — types only — and both keep the source checking cleanly in an editor that predates those definitions.

To work on it properly, scaffold from it and edit there:

vweb new /tmp/scratch

Then copy the change back, and into all example apps — they are scaffolded projects and a test holds them to it.

Examples

examples/todo-app is a real project — install it, run it, break it. It is also what the documentation quotes: cargo test --test docs fails if a page and the app disagree, so the snippets on the site cannot rot.

examples/minimal is a straight scaffold with no app-level customizations. It exists to keep one example app byte-close to the framework, so framework drift is obvious in tests.

The component library is documented on its own pages under docs/components/, each carrying its whole example. examples/ is for example apps.

Development

There is no manifest at the repository root — the Rust domain owns its own, so cargo runs inside cli/:

cd cli
cargo test          # unit + integration, all hermetic
cargo build --release

Integration tests drive the real binary: they scaffold into temp directories, run every generator, assert the results, and verify each destroy restores the file it touched. No network, no package manager.

Three direct dependencies — clap, include_dir, anyhow. Git operations shell out to the git binary; temp directories use std::env::temp_dir.

The repository is six domains side by side — cli/, framework/, library/, docs/, examples/ and spec/ — with no manifest at the root: cli/Cargo.toml is the whole Rust domain. The CLI embeds the two trees that sit outside it, so it is not publishable to crates.io and ships as a release binary instead.

framework/ and library/ are embedded at compile time. include_dir! embeds whatever is on disk, so install output in those trees would end up inside the binary. Working there is fine; the scaffolder and the registry skip node_modules, build output and lockfiles wherever they came from. Keep both trees clean when you commit, and expect a release build to warn if you do not.

More, including how a framework change reaches the examples and how to add a generator: contributing.

License

MIT — see LICENSE.