Business logic that survives
framework changes
MVVM is the architectural discipline. The browser is the platform. Web Loom packages give you clean, typed APIs over mature browser primitives — fetch, storage, routing, events — without heavy abstractions or framework lock-in. Your business logic runs anywhere.
Scaffold a full Web Loom app in one command
`create-web-loom` runs Vite, installs Web Loom packages, and replaces runnable starter files with a polished MVVM starter UI. It supports npm, pnpm, yarn, and bun.
npm create web-loom@latest my-appSupports Vite starters for React, Vue, Preact, Solid, Svelte, Lit, Vanilla, and Qwik in both TypeScript and JavaScript variants.
See create-web-loom docsThe pattern
One ViewModel.
Every framework.
The ViewModel is plain TypeScript — no framework imports, no heavy runtime. Infrastructure packages sit directly on mature browser APIs: fetch for HTTP, localStorage for persistence, History API for routing. Typed interfaces over what the platform already provides — not replacements for it.
- Model — owns data, calls browser APIs or your backend
- ViewModel — derives displayable state, exposes Commands
- View — subscribes and renders — the only framework-specific code
1export class CatalogModel extends BaseModel<CatalogProductDto[], any> {2 private readonly query = new QueryCore({3 cacheProvider: 'localStorage',4 defaultRefetchAfter: 2 * 60 * 1000,5 });6 private initialized = false;78 private async ensureInitialized(): Promise<void> {9 if (this.initialized) return;10 await this.query.defineEndpoint('catalog:products:v3', () => this.api.listProducts());11 this.query.subscribe('catalog:products:v3', (state) => this.syncFromQueryState(state));12 this.initialized = true;13 }1415 async fetchAll(forceRefetch = false): Promise<void> {16 await this.ensureInitialized();17 await this.query.refetch('catalog:products:v3', forceRefetch);18 }1920 private syncFromQueryState(state: EndpointState<CatalogProductDto[]>): void {21 this.setLoading(state.isLoading);22 if (state.data) this.setData(state.data);23 if (state.error) this.setError(state.error);24 }25}1export class CatalogViewModel extends ActiveAwareViewModel<CatalogModel> {2 private readonly productsState = signal<CatalogProductDto[]>([]);3 private readonly searchState = signal('');45 readonly filteredProducts = computed(() => {6 const q = this.searchState.get().trim().toLowerCase();7 const items = this.productsState.get();8 return q ? items.filter((p) => p.name.toLowerCase().includes(q)) : items;9 });1011 constructor(model: CatalogModel) {12 super(model);13 this.addSubscription(this.model.data$.subscribe((items) => this.productsState.set(items ?? [])));14 }1516 readonly refreshCatalogCommand = this.registerCommand(17 new Command(async () => this.model.refresh()),18 );1920 setSearchQuery(value: string): void {21 this.searchState.set(value);22 }23}1function CatalogRoute() {2 const products = useSignalValue(catalogViewModel.filteredProducts);3 const totalProducts = useSignalValue(catalogViewModel.totalProducts);4 const selectedProduct = useSignalValue(catalogViewModel.selectedProduct);5 const searchQuery = useSignalValue(catalogViewModel.searchQuery);6 const isLoading = useObservable(catalogViewModel.isLoading$, false);78 return (9 <ProductBrowser10 products={products}11 totalProducts={totalProducts}12 selectedProduct={selectedProduct}13 isLoading={isLoading}14 searchQuery={searchQuery}15 onSearchChange={(value) => catalogViewModel.setSearchQuery(value)}16 onRefresh={() => void catalogViewModel.refreshCatalogCommand.execute()}17 onAddToCart={(productId) => void cartViewModel.addToCartCommand.execute({ productId, quantity: 1 })}18 />19 );20}apps/ecommerce-mvvm/srcPlatform-first
The browser has matured.
Web Loom works with it, not around it.
Every infrastructure package is a thin, typed wrapper over a stable browser primitive. No proprietary runtimes. No invented protocols. Full tree-shaking.
Typed requests with interceptors and response mapping
Versioned, typed persistence with session and memory adapters
Reactive navigation state over pushState and popstate
Strongly typed event emitter with automatic disposal
Ecosystem
Explore the packages
Core Architecture
BaseModel, BaseViewModel, Commands — MVVM in plain TypeScript. No framework imports, fully testable.
Signals Core
Zero-dependency reactive primitives. Push-based state without a virtual DOM or framework runtime.
Query Core
Typed caching layer over fetch. Deduplication and stale-while-revalidate — no separate runtime.
Store Core
Minimal reactive store for UI-only state. localStorage adapter built in, no boilerplate.
Event Bus Core
Typed pub/sub over EventTarget primitives. Decoupled cross-feature messaging with zero overhead.
UI Core
Headless accessibility behaviors for dialog, list, and form. Bring your own markup.
Design Core
Design tokens as CSS custom properties. Flat and paper themes included — no CSS-in-JS required.
Package Roadmap
10 packages published on npm. 24 more in active development across forms, media, i18n, and more.
MVVM in Practice
A practical, code-first guide to Model-View-ViewModel architecture for modern frontend development.
Learn MVVM
Master framework-agnostic architecture
Learn MVVM patterns that work across React, Vue, Angular, Lit, and vanilla JavaScript. Every example is extracted from real, production-ready code in the Web Loom monorepo.
- Build Models that encapsulate business logic
- Create ViewModels that manage presentation state
- Implement Views that remain purely presentational
- Apply patterns like reactive state, event-driven communication, and design systems
Compatibility
Works with every major framework
The ViewModel has no framework imports. Connecting it to a new framework means writing one thin subscription bridge — typically under 20 lines.
The author
Festus Yeboah
Framework Architect
Festus is a frontend architect who has spent years watching teams rewrite the same business logic every time the framework pendulum swings. Web Loom is his attempt to give the web the same architectural continuity that Android, iOS, and .NET have enjoyed for twenty years.
The project is open-source and driven by the conviction that the 80% of an application that is not rendering code should be portable, testable, and immune to framework churn.
Follow on GitHubReady to scaffold and ship faster?
Start with the CLI starter, then evolve your ViewModels without framework lock-in.
npm create web-loom@latest my-app