Important
🤖 AI coding agents — start with AGENTS.md
The vendor-neutral entry point for this repo. Read it before building an app with wcstack or working on this monorepo. (Claude Code also auto-reads CLAUDE.md.)
What if the browser had these built in?
wcstack is a thought experiment turned into code. We imagine what future web standards could look like — reactive data binding, declarative routing, automatic component loading — and build them as if they already existed in the browser.
No framework. Just HTML tags that should exist.
🌐 wcstack.github.io — landing page, live demos, and the full package tour.
This project follows five strict constraints. They're what make it interesting.
| # | Rule | Why |
|---|---|---|
| 1 | Single CDN import | One <script> tag. That's it. No npm, no bundler, no config. |
| 2 | Features as custom tags | Everything is a custom element. If it can't be expressed as <wcs-something>, it doesn't belong here. |
| 3 | Initial load = tag definitions only | The script just registers custom elements. No initialization code, no bootstrap ritual. |
| 4 | Respect HTML semantics | Expressions live in data-* attributes and text nodes — places HTML already allows extension. The DOM structure and semantics stay intact. |
| 5 | Latest ECMAScript | We actively adopt cutting-edge JS features. No transpiling to ES5. This is the future, after all. |
These rules sound simple. They're not.
Respecting HTML semantics means you need to deeply understand where the spec allows extension — and where it doesn't. Building everything as custom tags means solving lifecycle, ordering, and communication within the Custom Elements spec. No dependencies means every algorithm is yours to write. And it all has to feel like it could be a browser built-in.
In every existing framework, the component is where UI meets state. Even with external stores, you still write glue code inside the component to pull state in. State and UI always couple through JavaScript.
wcstack takes a different path. Literally.
The only contract between UI and state is a path string — user.name, cart.items.*.subtotal, @shared. No hooks. No imports. No glue code. The component's JavaScript doesn't contain a single line that references state. The HTML alone describes every data dependency.
State ← "user.name" → UI Path binds the two layers
Comp A ← "@app" → Comp B Named path crosses components
Loop ← "items.*" → Template Wildcard abstracts the index
This means you can redesign the UI without touching state, refactor state without touching the DOM, and read the HTML to understand everything. It's the same idea as a REST URL — a simple string contract, no shared code.
Working with an AI coding agent — Claude Code, Codex, Cursor, Copilot, or anything else? Point it at AGENTS.md first. It is the vendor-neutral entry point for this repository, and it covers both directions:
- Building an app with wcstack — install the wcstack-app skill for the complete
data-wcsbinding syntax and tag catalog, then gate generated HTML withnpx @wcstack/lint --errors-onlyand iterate until it exits0. - Working on this monorepo — package layout and commands, generated-file rules, and the protocols that must not be changed casually.
Claude Code reads CLAUDE.md (the more detailed, tool-specific guide) automatically; tell every other agent to start at AGENTS.md.
Forty-one independent runtime packages + one tooling extension package. Zero runtime dependencies (except happy-dom for SSR). No build step required.
@wcstack/state — Declare state inline, bind it to the DOM with attributes.
<wcs-state>
<script type="module">
export default {
taxRate: 0.1,
cart: {
items: [
{ name: "Widget", price: 500, quantity: 2 },
{ name: "Gadget", price: 1200, quantity: 1 }
]
},
removeItem(event, index) {
this["cart.items"] = this["cart.items"].toSpliced(index, 1);
},
get "cart.items.*.subtotal"() {
return this["cart.items.*.price"] * this["cart.items.*.quantity"];
},
get "cart.total"() {
return this.$getAll("cart.items.*.subtotal", []).reduce((a, b) => a + b, 0);
},
get "cart.grandTotal"() {
return this["cart.total"] * (1 + this.taxRate);
}
};
</script>
</wcs-state>
<template data-wcs="for: cart.items">
<div>
{{ .name }} ×
<input type="number" data-wcs="value: .quantity">
= <span data-wcs="textContent: .subtotal|locale"></span>
<button data-wcs="onclick: removeItem">Delete</button>
</div>
</template>
<p>Grand Total: <span data-wcs="textContent: cart.grandTotal|locale(ja-JP)"></span></p>- Path getters —
get "users.*.fullName"()computed properties at any depth - Structural directives —
for,if/elseif/elsevia<template> - 40+ built-in filters — comparison, arithmetic, string, date, formatting
- Two-way binding — automatic for
<input>,<select>,<textarea> - Mustache syntax —
{{ path|filter }}in text nodes - Web Component binding — bidirectional state sync with Shadow DOM
@wcstack/router — Define your app's navigation structure in markup.
<wcs-router>
<template>
<wcs-route path="/">
<wcs-layout layout="main-layout">
<nav slot="header">
<wcs-link to="/">Home</wcs-link>
<wcs-link to="/products">Products</wcs-link>
</nav>
<wcs-route index>
<wcs-head><title>Home</title></wcs-head>
<app-home></app-home>
</wcs-route>
<wcs-route path="products">
<wcs-route index>
<product-list></product-list>
</wcs-route>
<wcs-route path=":id(int)">
<product-detail data-bind="props"></product-detail>
</wcs-route>
</wcs-route>
</wcs-layout>
</wcs-route>
<wcs-route fallback>
<error-404></error-404>
</wcs-route>
</template>
</wcs-router>
<wcs-outlet></wcs-outlet>- Nested routes & layouts — compose UI declaratively with Light DOM
- Typed parameters —
:id(int),:slug(slug),:date(isoDate)with auto-conversion - Auto-binding — inject URL params into components via
data-bind - Head management —
<wcs-head>switches<title>and<meta>per route - Navigation API — built on the modern standard with popstate fallback
- Route guards — protect routes with async decision functions
@wcstack/fetch — Declarative HTTP communication as a headless Web Component.
<wcs-state>
<script type="module">
export default {
users: [],
loading: false,
filterRole: "",
get usersUrl() {
const role = this.filterRole;
return role ? "/api/users?role=" + role : "/api/users";
},
};
</script>
</wcs-state>
<!-- URL changes automatically trigger re-fetch -->
<wcs-fetch data-wcs="url: usersUrl; value: users; loading: loading"></wcs-fetch>
<template data-wcs="if: loading">
<p>Loading...</p>
</template>
<template data-wcs="for: users">
<div data-wcs="textContent: .name"></div>
</template>- CSBC architecture — Core / Shell / Binding Contract separation
- wc-bindable-protocol — works with React, Vue, Svelte, Solid via thin adapters
- URL observation — auto re-fetch when bound URL changes
- Trigger property — declarative fetch execution from state, no DOM refs
- HTML replace mode — htmx-like
targetattribute for server-rendered fragments - Headless Core —
FetchCoreruns in Node.js, Deno, Cloudflare Workers
@wcstack/autoloader — Write a tag, it loads. No registration needed.
<script type="importmap">
{
"imports": {
"@components/ui/": "./components/ui/",
"@components/ui|lit/": "./components/ui-lit/"
}
}
</script>
<!-- Auto-loaded from ./components/ui/button.js -->
<ui-button></ui-button>
<!-- Auto-loaded with Lit loader from ./components/ui-lit/card.js -->
<ui-lit-card></ui-lit-card>- Import Map based — namespace resolution, no per-component registration
- Eager & lazy loading — load critical components first, the rest on demand
- MutationObserver — dynamically added elements are auto-detected
- Pluggable loaders — mix Vanilla, Lit, or any custom loader
isattribute — customized built-in elements with autoextendsdetection
@wcstack/server — Same HTML, server-rendered. No special syntax needed.
import { renderToString } from "@wcstack/server";
const html = await renderToString(`
<wcs-state enable-ssr>
<script type="module">
export default {
items: [],
async $connectedCallback() {
const res = await fetch("/api/items");
this.items = await res.json();
}
};
</script>
</wcs-state>
<template data-wcs="for: items">
<div data-wcs="textContent: items.*.name"></div>
</template>
`);- Drop-in SSR — add
enable-ssrto<wcs-state>, callrenderToString(). Done. - Automatic hydration — client picks up where the server left off, zero flicker
- Relative URL resolution —
baseUrloption makesfetch("/api/...")work on the server - Version-safe fallback — on version mismatch, DOM is cleaned up and CSR kicks in
<wcs-ssr>hydration data — state snapshots, templates, and properties in one element
@wcstack/websocket— Declarative real-time communication with<wcs-ws>and bindable connection/message state.@wcstack/upload— Declarative file upload flows with progress, status, and framework-agnostic bindings.@wcstack/storage— Declarative persistence with<wcs-storage>for localStorage / sessionStorage state sync.@wcstack/timer— Declarative timers with<wcs-timer>for ticking, elapsed time, and state-driven polling.@wcstack/raf— Declarative requestAnimationFrame with<wcs-raf>: frame ticks, first-classdt, and asuspendedoutput for hidden tabs.@wcstack/geolocation— Declarative geolocation with<wcs-geo>for one-shot/continuous position, accuracy, and live permission state.@wcstack/debounce— Declarative debounce/throttle with<wcs-debounce>and<wcs-throttle>for coalescing value and signal streams.@wcstack/clipboard— Declarative clipboard with<wcs-clipboard>for read/write, richClipboardItems, copy/cut/paste monitoring, and live permission state.@wcstack/broadcast— Declarative cross-tab messaging with<wcs-broadcast>for same-origin BroadcastChannel pub/sub as bindable state.@wcstack/worker— Declarative Web Worker with<wcs-worker>for offloading work to a background thread as bindable message/error/running state.@wcstack/sse— Declarative Server-Sent Events with<wcs-sse>for one-way streaming (EventSource) as bindable message/connection state, with named-event support.@wcstack/intersection— Declarative IntersectionObserver with<wcs-intersect>for lazy-loading, infinite scroll, and scrollspy as bindable visibility state.@wcstack/wakelock— Declarative Screen Wake Lock with<wcs-wakelock>that keeps the screen awake while a bound boolean is true, re-acquiring across visibility changes.@wcstack/resize— Declarative ResizeObserver with<wcs-resize>for element size, container-width probing, and size-dependent logic as bindable state.@wcstack/speech— Declarative speech with<wcs-speak>(text-to-speech as a command-token) and<wcs-listen>(recognition results as event-token state).@wcstack/permission— Declarative Permissions API monitor with<wcs-permission>exposing livegranted/denied/promptstate. Read-only watcher (no commands); pairs with feature nodes like<wcs-geo>.@wcstack/network— Declarative Network Information monitor with<wcs-network>exposing liveeffectiveType/downlink/rtt/saveDatastate for adaptive loading. Read-only watcher (no commands, no attributes); unsupported (Firefox/Safari) is the common case, not an edge case.@wcstack/screen-orientation— Declarative Screen Orientation monitor +lock/unlockcommands with<wcs-screen-orientation>exposingtype/angle/portrait/landscape. Monitoring needs no_genguard (synchronous);lock()does (async, independent of monitoring).@wcstack/fullscreen— Declarative Fullscreen API with<wcs-fullscreen target="...">, reusing<wcs-intersect>'s target-resolution pattern.activetracks whether the resolved target is the document'sfullscreenElement.@wcstack/picture-in-picture— Declarative Picture-in-Picture with<wcs-pip target="...">(target must be a<video>element). Same target-resolution pattern as<wcs-fullscreen>.@wcstack/pointer-lock— Declarative Pointer Lock with<wcs-pointer-lock target="...">for games/canvas UIs.movementX/movementYintentionally out of scope in v1 (pair with@wcstack/debounce/@wcstack/throttleif added later).@wcstack/share— Declarative Web Share API with<wcs-share>:share(data)command,value/loading/error/cancelledstate.cancelled(user dismissed the share sheet) is kept separate fromerror(a true failure).@wcstack/eyedropper— Declarative EyeDropper API (desktop color picker) with<wcs-eyedropper>:open()/abort()commands,valueas{ sRGBHex }. Samevalue/loading/error/cancelledshape as<wcs-share>.@wcstack/contacts— Declarative Contact Picker API with<wcs-contacts>:select(properties, options)command (Android Chrome only — unsupported is the default elsewhere).valueis always an array, even withmultiple: false.@wcstack/credential— Declarative Credential Management (password/federated only — WebAuthn is explicitly out of scope) with<wcs-credential>:get(options)/store(credential)commands sharing one_gen(documented concurrency limitation).@wcstack/idle— Declarative Idle Detection with<wcs-idle>: gesture-gatedrequestPermission()+start/stop, exposinguserState/screenState/active. Does not duplicate permission state — compose with<wcs-permission name="idle-detection">. Does not auto-start on connect.@wcstack/tilt— Declarative Device Orientation with<wcs-tilt>, absorbing iOS's gesture-gatedrequestPermission()(a no-op elsewhere) so callers write one flow that works everywhere.permissionStateis a 3-value vocabulary tracked locally (no matching Permissions API entry exists).@wcstack/accelerometer/@wcstack/gyroscope/@wcstack/magnetometer/@wcstack/ambient-light-sensor— The Generic Sensor API family:<wcs-accelerometer>/<wcs-gyroscope>/<wcs-magnetometer>exposex/y/z;<wcs-ambient-light-sensor>exposes a singleilluminancescalar (and has the weakest browser support — fingerprinting mitigations have disabled it in some browsers). All four compose with<wcs-permission name="...">rather than duplicating permission state, and need no_genguard (synchronous start/stop) beyond a guarded sensor constructor call.@wcstack/notification— Declarative desktop notifications with<wcs-notify>: show via command-token (notify), click back via event-token (clicked) — both directions in one tag. Self-contained permission, Service Worker fallback for mobile.@wcstack/defined— Declarative custom-element readiness with<wcs-defined>: watcheswhenDefined()for a set of tags and exposesdefined/pending/missing/count/totalstate, with timeout-based load-failure detection. Companion to the autoloader; what CSS:definedcannot do.@wcstack/camera— Declarative camera capture and recording with<wcs-camera>(getUserMedia + built-in preview) and<wcs-recorder>(MediaRecorder). The liveMediaStreamis bound straight to elements via a command-token argument and never stored in serializable state — only derived values (permission, recording flag, the recordedBlob/URL) flow through state.@wcstack/signals— A signals-based, fine-grained reactive core (the JS-first counterpart to@wcstack/state):signal/computed/effect, asyncresource/streamResource, keyedFor/Index, and abindNodeadapter that drives the same wc-bindable IO nodes through signals. TC39-Signals-shaped, zero-dependency.@wcstack/devtools— In-page DevTools overlay with<wcs-devtools>: inspect state trees (with inline editing through the normal reactive pipeline), see which DOM nodes each path is wired to, and watch a live timeline of writes, update batches, and command/event-token emissions — including zero-subscriber "empty emits". One script tag, connects via the DevTools Hook Protocol, zero-dependency.@wcstack/lint— Static-contract validator CLI (npx @wcstack/lint, command namewcs-validate): checks HTMLdata-wcsbindings andwcstack.manifest.jsonsidecars headlessly with the same validator core as the VS Code extension — identical diagnostic codes and ranges in IDE and CI, stable exit-code contract for generate–validate–fix loops. Zero-dependency.wcstack-intellisense— VS Code extension that provides language support for<wcs-state>inline scripts.
<!DOCTYPE html>
<html>
<head>
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
</head>
<body>
<wcs-state>
<script type="module">
export default {
count: 0,
countUp() { this.count++; }
};
</script>
</wcs-state>
<p>Count: {{ count }}</p>
<button data-wcs="onclick: countUp">+1</button>
</body>
</html>One <script> tag. One custom element. Pure HTML. That's it.
Every I/O node reflects its boolean output states (loading, connected, error, granted, …) into CustomStateSet, so plain CSS can react to component state — no JavaScript required:
wcs-fetch:state(loading) ~ .spinner { display: block; }
form:has(wcs-fetch:state(error)) .msg { display: block; }
wcs-ws:state(connected) ~ .indicator { color: limegreen; }
wcs-permission:state(denied) ~ .help { display: block; }Each package README lists its reflected states. Supported in Chrome/Edge 125+, Safari 17.4+, Firefox 126+; in older browsers the styles simply don't apply — the components keep working. States are not serialized into SSR output (combine with wcs-x:not(:defined) for first-paint styling).
For debugging, add the debug-states attribute to a tag to mirror its states as data-wcs-state-* attributes in the DevTools Elements panel, or read the debugStates property. Write production CSS against :state(), not those attributes.
wcstack/
├── packages/
│ ├── state/ # @wcstack/state
│ ├── router/ # @wcstack/router
│ ├── fetch/ # @wcstack/fetch
│ ├── autoloader/ # @wcstack/autoloader
│ ├── server/ # @wcstack/server
│ ├── storage/ # @wcstack/storage
│ ├── timer/ # @wcstack/timer
│ ├── raf/ # @wcstack/raf
│ ├── geolocation/ # @wcstack/geolocation
│ ├── websocket/ # @wcstack/websocket
│ ├── upload/ # @wcstack/upload
│ ├── debounce/ # @wcstack/debounce
│ ├── clipboard/ # @wcstack/clipboard
│ ├── broadcast/ # @wcstack/broadcast
│ ├── worker/ # @wcstack/worker
│ ├── sse/ # @wcstack/sse
│ ├── intersection/ # @wcstack/intersection
│ ├── wakelock/ # @wcstack/wakelock
│ ├── resize/ # @wcstack/resize
│ ├── speech/ # @wcstack/speech
│ ├── permission/ # @wcstack/permission
│ ├── network/ # @wcstack/network
│ ├── screen-orientation/ # @wcstack/screen-orientation
│ ├── fullscreen/ # @wcstack/fullscreen
│ ├── picture-in-picture/ # @wcstack/picture-in-picture
│ ├── pointer-lock/ # @wcstack/pointer-lock
│ ├── share/ # @wcstack/share
│ ├── eyedropper/ # @wcstack/eyedropper
│ ├── contacts/ # @wcstack/contacts
│ ├── credential/ # @wcstack/credential
│ ├── idle/ # @wcstack/idle
│ ├── tilt/ # @wcstack/tilt
│ ├── accelerometer/ # @wcstack/accelerometer
│ ├── gyroscope/ # @wcstack/gyroscope
│ ├── magnetometer/ # @wcstack/magnetometer
│ ├── ambient-light-sensor/ # @wcstack/ambient-light-sensor
│ ├── notification/ # @wcstack/notification
│ ├── defined/ # @wcstack/defined
│ ├── camera/ # @wcstack/camera
│ ├── signals/ # @wcstack/signals
│ ├── devtools/ # @wcstack/devtools
│ ├── lint/ # @wcstack/lint
│ └── vscode-wcs/ # wcstack-intellisense (VS Code extension)
Each package is independently built, tested, and published.
Examples under examples/ track the packages that still live in this repository. The former AI/Auth0 demos moved to @csbc-dev/ai-agent and @csbc-dev/auth0, and are no longer included here. The legacy npm packages @wcstack/ai and @wcstack/auth0 are deprecated.
Commands run from within a specific package directory (e.g., packages/state/):
npm run build # Clean dist, compile TypeScript, bundle with Rollup
npm test # Run tests (Vitest)
npm run test:coverage # Coverage (100% statements/functions/lines, 97%+ branches)
npm run lint # ESLintMIT