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, and thirty-plus Web APIs as plain HTML tags — and build them as if they already existed in the browser.

未来のWeb標準を想像し、それがすでにブラウザに存在するかのようにコードにする思考実験。30以上のWeb APIをHTMLタグに。

39
Packages
30+
Web API Tags
0
Dependencies
1
Script Tag Each

The only contract is a path string

No hooks. No imports. No glue code.

フックなし。インポートなし。グルーコードなし。

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 stringuser.name, cart.items.*.subtotal, @shared. The component's JavaScript doesn't contain a single line that references state. The HTML alone describes every data dependency — the same idea as a REST URL: a simple string contract, no shared code.

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

UIと状態を結ぶ唯一の契約はパス文字列。UIを作り直しても状態に触れなくていい。状態をリファクタリングしてもDOMに触れなくていい。HTMLを読めばすべてが分かる。

HTML tags that should exist

What's missing from the browser — and what we built.

ブラウザに足りないもの — そして我々が作ったもの。

<wcs-state>

Reactive State

What if HTML had built-in reactive data binding?

Reactive state with Mustache syntax, path getters for computed properties, 40+ built-in filters, two-way binding, and structural directives — no virtual DOM.

Mustache構文、パスゲッター、40以上のビルトインフィルタ、双方向バインディング、構造ディレクティブ対応のリアクティブ状態管理。

  • Mustache Syntax
  • Path Getters
  • 40+ Filters
  • Two-way Binding
  • for / if / else
<wcs-router>

Declarative Router

What if you could define SPA routes directly in HTML?

Define routes with nested layouts, typed URL parameters (:id(int)), route guards, and per-route <head> management. Built on the Navigation API.

ネストレイアウト、型付きURLパラメータ、ルートガード、ルート別head管理を備えた宣言的ルーティング。

  • Nested Routes
  • Typed Params
  • Layouts
  • Route Guards
  • Head Management
<wcs-autoloader>

Component Autoloader

What if the browser could auto-import components just by seeing their tags?

Detects custom elements in the DOM and dynamically imports them via Import Maps. Supports eager & lazy loading with MutationObserver for dynamically added elements.

DOMのカスタム要素を検出し、Import Mapsで動的インポート。Eager/Lazy読み込みとMutationObserverに対応。

  • Import Maps
  • Eager / Lazy
  • MutationObserver
  • Pluggable Loaders
@wcstack/signals

Signals Core

What if TC39 signals were already here — and drove the same tags?

A fine-grained reactive core — signal / computed / effect, async resources, keyed For / Index — the JS-first counterpart to <wcs-state>, driving the same bindable tags.

signal / computed / effect、非同期resource、キー付きFor/Indexを備えたシグナルベースのリアクティブコア。stateのJSファースト版。

  • TC39-shaped
  • Async Resources
  • Keyed For / Index
  • bindNode Adapter
@wcstack/server

Server-Side Rendering

What if your templates rendered on the server — unchanged?

Same HTML, server-rendered. Add enable-ssr, call renderToString(), done. Automatic hydration with zero flicker, and version-safe fallback to CSR.

同じHTMLをサーバーでレンダリング。enable-ssrを付けてrenderToString()を呼ぶだけ。ゼロフリッカーの自動ハイドレーション付き。

  • Drop-in SSR
  • Auto Hydration
  • Zero Flicker
  • CSR Fallback
<wcs-fetch>

Fetch as a Tag

What if fetch was a tag?

Declarative HTTP as a headless component. Bind a URL, get data — URL changes automatically re-fetch. htmx-like HTML replace mode, and a headless core that runs in Node.js, Deno, and Workers.

宣言的HTTP通信。URLをバインドすればデータが届く。URL変更で自動再フェッチ。htmxライクなHTML置換モードも。

  • URL Observation
  • Trigger Property
  • HTML Replace
  • Headless Core

What if every Web API was a tag?

Thirty-plus declarative wrappers over the Web platform — each one a headless custom element that binds straight into your state.

Webプラットフォームの30以上のAPIを宣言的タグ化。すべてヘッドレスなカスタム要素として、状態に直接バインドされる。

<wcs-fetch>HTTP requests, auto re-fetch
<wcs-storage>localStorage / sessionStorage sync
<wcs-upload>File upload with progress
<wcs-ws>WebSocket real-time comms
<wcs-sse>Server-Sent Events streaming
<wcs-broadcast>Cross-tab messaging
<wcs-worker>Web Worker offloading
<wcs-timer>Ticks, elapsed time, polling
<wcs-debounce> <wcs-throttle>Value & signal coalescing
<wcs-clipboard>Clipboard read / write / monitor
<wcs-geo>Geolocation, live permission
<wcs-permission>Permissions API monitor
<wcs-notify>Desktop notifications
<wcs-intersect>Visibility, lazy-load, scrollspy
<wcs-resize>Element size observation
<wcs-wakelock>Screen Wake Lock
<wcs-camera> <wcs-recorder>Camera capture & recording
<wcs-speak> <wcs-listen>Speech synthesis & recognition
<wcs-defined>Custom-element readiness gate
<wcs-fullscreen>Fullscreen API
<wcs-pip>Picture-in-Picture
<wcs-pointer-lock>Pointer Lock for games / canvas
<wcs-screen-orientation>Orientation monitor & lock
<wcs-idle>Idle Detection
<wcs-network>Network Information monitor
<wcs-share>Web Share sheet
<wcs-contacts>Contact Picker
<wcs-credential>Credential Management
<wcs-eyedropper>EyeDropper color picker
<wcs-tilt>Device Orientation, iOS-safe
<wcs-accelerometer> <wcs-gyroscope>Motion sensors (x / y / z)
<wcs-magnetometer> <wcs-ambient-light-sensor>Magnetic field & illuminance

One protocol behind them all: wc-bindable

Every I/O node exposes a static manifest — observable properties, settable inputs, invocable commands — so any binding core can discover and wire it generically. <wcs-state> and @wcstack/signals speak it natively; thin adapters connect React, Vue, Svelte, and Solid.

すべてのI/Oノードは静的マニフェスト(properties / inputs / commands)を公開。stateとsignalsはネイティブ対応、薄いアダプタでReact / Vue / Svelte / Solidからも使える。

Code that reads like HTML

Because it is HTML.

HTMLのように読める。なぜなら、HTMLだから。

index.html State
<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 }} &times;
    <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>
index.html Router
<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 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>
index.html Fetch
<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>
server.js SSR
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>
`);
// Client hydrates automatically — zero flicker.
index.html Autoloader
<!-- Define your import maps -->
<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 -->
<ui-lit-card></ui-lit-card>

Rules of the Game

This project follows five strict constraints. They're what make it interesting.

5つの厳格な制約。これが面白さの源泉。

1

Single CDN import

One <script> tag. That's it. No npm, no bundler, no config.

scriptタグ1つ。npm不要、バンドラー不要、設定不要。

2

Features as custom tags

Everything is a custom element. If it can't be expressed as <wcs-something>, it doesn't belong here.

すべてはカスタム要素。<wcs-something>で表現できなければ、ここには属さない。

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.

式はdata-*属性とテキストノードに配置 — HTMLが拡張を許可する場所のみ。DOM構造とセマンティクスはそのまま。

5

Latest ECMAScript

We actively adopt cutting-edge JS features. No transpiling to ES5. This is the future, after all.

最新のJS機能を積極採用。ES5へのトランスパイルなし。未来のプロジェクトだから。

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.

シンプルに聞こえるルールだが、実際は違う。HTMLセマンティクスを尊重するなら、仕様が拡張を許す場所と許さない場所を深く理解する必要がある。すべてをカスタムタグで構築するなら、Custom Elementsの仕様の中でライフサイクル・順序・通信を解決しなければならない。依存ゼロなら、あらゆるアルゴリズムを自分で書く。そしてそのすべてが「ブラウザ組み込みかもしれない」と感じさせなければならない。

One script tag. That's the rule.

Pick what you need. Every package works standalone and ships an /auto entry.

必要なものだけ選ぶ。全パッケージが単独で動作し、/autoエントリを持つ。

index.html CDN via esm.run
<!-- Pick any of the 39 packages — same pattern for all -->
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/router/auto"></script>
<script type="module" src="https://esm.run/@wcstack/fetch/auto"></script>
<script type="module" src="https://esm.run/@wcstack/autoloader/auto"></script>

Each script registers its custom elements and does nothing else. No initialization, no bootstrap — tags activate when the browser parses them.

各スクリプトはカスタム要素を登録するだけ。初期化もブートストラップもなし — ブラウザがパースした時にタグが起動する。

Editor support: the wcstack-intellisense VS Code extension brings TypeScript language features — completion, diagnostics, hover — to <wcs-state> inline scripts in your HTML.

VS Code拡張「wcstack-intellisense」が、HTML内の<wcs-state>インラインスクリプトに補完・診断・ホバーを提供。