Decorator-driven web components that started as a meme and accidentally became useful.
Loom was born out of pure spite for boilerplate. What began as an ironic "what if decorators did everything?" experiment turned into a genuinely useful framework for building web components.
It powers placing.space in production — a real-time collaborative pixel canvas — so it's been battle-tested with WebSocket streams, thousands of DOM nodes, and zero framework overhead.
Most frameworks make you choose: lightweight or batteries-included. Loom doesn't.
- ~20KB gzipped baseline — smaller than React + ReactDOM alone, but includes a router, DI container, reactivity system, and lifecycle management out of the box.
- No virtual DOM — JSX compiles to real DOM nodes. The morpher diffs the live DOM directly and patches in-place. No throwaway object trees, no GC pressure, no overhead on top of the work you were going to do anyway.
- Zero dependencies — the entire framework is one
package.jsonentry. No version conflicts, no transitive supply chain risk, no "which router do we pick" debates. - Pay-as-you-go — built-in components (
<loom-virtual>,<loom-canvas>,<loom-icon>,<loom-image>) and systems (@form,@api,CollectionStore) are tree-shaken if you don't import them. You only ship what you use. - Web standards — built on custom elements, Shadow DOM, and TC39 Stage 3 decorators. When browsers ship native decorator support, your code gets faster for free.
@component/@styles— register custom elements and scoped styles in one line@reactive/@prop— fine-grained reactivity that only re-renders what changed@computed/@watch— derived state and side effects@on/@emit— declarative event handling via typed event bus- JSX + DOM morphing — write JSX, get surgical DOM patches (no virtual DOM)
@inject/@service/@factory— full dependency injection container- Hash & history router —
@route,@guard,@group,<loom-outlet>,<loom-link> @api/@intercept— declarative data fetching with SWR, retry, and Result combinators@lazy— code-split components with one decorator@catch_/@suspend— error boundaries and async loading state@interval/@timeout/@debounce/@throttle/@animationFrame— auto-cleaned timing@mount/@unmount— lifecycle hooks@form— declarative form binding with validation@transform— typed value transforms for props and route paramsReactive<T>/CollectionStore<T>— observable state withLocalAdapterpersistencecss\`` — adopted stylesheets with zero FOUC<loom-virtual>— virtualized list for huge datasetscreateDecorator— build your own decorators with the same factory Loom uses- Zero dependencies — just TypeScript and the platform
npm create @toyz/loom my-app
cd my-app
npm install
npm run devOr install manually:
npm install @toyz/loomimport { LoomElement, component, reactive, css, styles } from "@toyz/loom";
const counterStyles = css`
button {
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
}
span {
font-weight: bold;
margin-left: 0.5rem;
}
`;
@component("click-counter")
@styles(counterStyles)
class ClickCounter extends LoomElement {
@reactive accessor count = 0;
update() {
return (
<button onClick={() => this.count++}>
Clicks: <span>{this.count}</span>
</button>
);
}
}<click-counter></click-counter>
<script type="module" src="./main.ts"></script>Loom uses TC39 decorators, which require es2022 or later. Point your config at the Loom JSX runtime:
{
"compilerOptions": {
"target": "es2022",
"jsx": "react-jsx",
"jsxImportSource": "@toyz/loom"
}
}For Vite:
// vite.config.ts
export default defineConfig({
esbuild: {
target: "es2022",
jsx: "automatic",
jsxImportSource: "@toyz/loom",
},
});Full documentation with interactive examples:
MIT — do whatever you want with it.