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.

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

Custom Elements Single CDN import HTML-semantic syntax Zero dependencies

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

Code that reads like HTML

Because it is HTML.

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

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>
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 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>

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. Each tag works standalone.

必要なものだけ選ぶ。各タグは単独で動作する。

index.html CDN via esm.run
<!-- Pick one, two, or all three -->
<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/autoloader/auto"></script>

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

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