A calm native-first web framework for sites and apps.
Web Components · Signals · Browser-rendered static snapshots · Zero runtime dependencies
A calm native-first web framework for sites and apps.
Website · Documentation · Mado UI
Build with real Web Components, signals, routing, data and forms. Ship live SPAs and browser-rendered static documents from one component model.
One component model. One page model. One release command.
Mado (窓) means window in Japanese: a calm window into your app,
without dragging a whole frontend factory into the room.
Mado component = Custom Element + open Shadow DOM
Mado page = route + load + head + view + optional static declaration
Mado release = Vite build
+ browser-rendered static documents
+ Declarative Shadow DOM
+ SPA fallback
+ deployment artifact
Client activation = atomic takeover
≠ hydration
≠ SSR reconciliationBrowser-native source, with Vite as the development and delivery transport. No framework-specific compiler and zero runtime dependencies.
- Public landing pages
- Documentation sites
- Product / catalog pages
- SaaS applications
- Business applications
- Admin panels and internal tools
- Dashboards
- Embedded widgets
Requires Node.js 22.12 or newer.
npm exec --yes --package @madojs/mado@latest -- mado init my-app
cd my-app
npm install
npm run devThe default starter is the universal starter: ~15 source files, runnable without a backend, demonstrating one Shadow Component shared between a static landing page and a live SPA route.
Evaluating a larger business frontend with an auth shell, guarded zones and explicit module boundaries? The optional modular experiment is available:
npm exec --yes --package @madojs/mado@latest -- \
mado init my-app --starter modularimport { signal, computed, effect } from "@madojs/mado";
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => console.log(count()));
count.set(1);html`<button @click=${fn} ?disabled=${loading} class=${cls}>${label}</button>`;${value}— child content (text, nodes, arrays, nestedhtml,each)@event=${fn}— event listenerattr=${v}— attribute.prop=${v}— DOM property?attr=${flag}— boolean attribute- Functions and signals are tracked reactively
import { component, css, html } from "@madojs/mado";
component(
"x-card",
() => html`<section><slot></slot></section>`,
{
styles: css`:host { display: block; padding: 1rem; }`,
},
);Open shadow root, scoped styles, slots, attribute reactivity, a real custom element under the hood. The same component renders inside the static snapshot via Declarative Shadow DOM and inside the live SPA via direct DOM attachment.
import { html, page } from "@madojs/mado";
export default page({
static: true, // capture as HTML at release
title: "Mado Keyboard",
head: () => ({ description: "A canonical product page." }),
view: () => html`<h1>Welcome</h1>`,
});import { routes, routeUrl } from "@madojs/mado";
export default routes({
"/": () => import("./pages/home.page"),
"/users/:id": () => import("./pages/user.page"),
"*": () => import("./pages/not-found.page"),
});
// Internal links must be base-aware.
html`<a data-link href=${routeUrl("/users/42")}>User</a>`;Use data-link for normal SPA navigation. A destination that requires a new
CSP/COOP/auth document realm still uses routeUrl() but omits data-link so
the browser performs a full document request.
Lazy loading, layout groups, query params, guards, pointer/focus prefetch,
per-entry scroll restoration, commit-aware fragment links, error boundary,
opt-in View Transitions, base-path awareness (Vite base → runtime
import.meta.env.BASE_URL). Reactive
queryParam() values follow programmatic navigation, data-link and browser
history changes.
Layouts are route-owned by default. A shell that must preserve its actual DOM
and lifecycle across child pages can opt into a retained outlet with
layout({ lifecycle: "group", layout, guard?, routes }); guards still run per
pathname/query and old leaf pages are never cached.
import { resource, mutation, jsonFetcher } from "@madojs/mado";
const user = resource(
() => `/api/users/${userId()}`,
jsonFetcher<User>(),
{ staleTime: 60_000 },
);
const save = mutation((user, signal) => api.saveUser(user, { signal }), {
invalidates: ["/api/users*"],
});Cache, loading/error state, abort, refresh, optimistic mutate(),
glob-based invalidation. Resources and mutations created by pages/components
are lifecycle-owned; standalone instances expose idempotent dispose().
import { useForm, html } from "@madojs/mado";
const form = useForm({
initial: { email: "", age: "" as number | "" },
});
html`<form @submit=${form.onSubmit(save)}>
<input name="email" type="email" required @input=${form.onInput} />
<input name="age" type="number" min="18" @input=${form.onInput} />
<button type="submit">Save</button>
</form>`;HTML owns constraints and keyboard/form semantics; Mado supplies typed values,
errors, touched/dirty state and abortable async validation. Applications can
normalize authoritative server validation into form.setErrors() without
coupling Mado to a backend response format.
mado releasemado release runs your app in a real Chromium and freezes the
rendered HTML — including the Shadow DOM via Declarative Shadow DOM —
into one file per route. On first paint Mado atomically replaces the
static tree with the live tree: no hydration protocol, no node
reconciliation, no per-attribute diffing.
- Real search engines see a fully rendered document.
- Social preview bots see the canonical / og tags inside the raw HTML.
- Same-origin assets and module preloads remain base-relative and portable.
- JS-disabled browsers see meaningful content.
- The live app boots from the same snapshot without re-fetching seeded data.
mado init my-app # scaffold universal starter
mado init my-app --starter modular # scaffold optional modular experiment
mado dev # Vite dev server
mado build # Vite production SPA build
mado typecheck # tsc --noEmit
mado static [--base-url …] # low-level snapshot only
mado release # vite build + snapshots + deployment files
mado preview # serve out/ like a real static host
mado new <kind> <path> # scaffold canonical filesAll CLI records share level, scope, code, message, data and a
timestamp. Use --log-level, --log-format=pretty|plain|json,
MADO_LOG_LEVEL, MADO_LOG_FORMAT or NO_COLOR for automation.
if (import.meta.env.DEV) {
const { devtools } = await import("@madojs/mado/devtools.js");
devtools.open();
}The development-only Shadow DOM overlay is toggled with Alt+Shift+M and
inspects reactivity, components, routing, data and structured diagnostics.
Load its public subpath only in development so the overlay itself does not
become part of the production application.
@madojs/ui is a separate development CLI and
versioned source registry. Its live catalog documents the source,
dependencies, accessibility and fallback contract for each reviewed
foundation, component, block and template. The CLI copies selected source into
the application; the application owns the resulting files.
npx @madojs/ui@latest init
npx @madojs/ui@latest list
npx @madojs/ui@latest add button field panel
npx @madojs/ui@latest remove panel --dry-runApplications never import @madojs/ui in browser code. mado new component
creates a minimal application-owned component skeleton; mado-ui add resolves
reviewed registry source and its dependencies. Lock format 2 records explicit
installation roots and their dependency edges so updates and removals do not
guess ownership. Legacy format-1 locks require an explicit
mado-ui migrate <explicit-item...>; use the dry run first and never infer the
roots. See the Mado UI guide or the
registry source.
- No server renderer.
- No hydration protocol.
- No framework compiler.
- No runtime dependencies.
- No built-in backend.
- No bundled UI runtime. Mado UI provides a focused, copy-owned official registry; the broader third-party ecosystem is still early.
- Modern evergreen browsers only.
- A compatible Chromium is required at release time for static routes.
- Static
paths()andinitialData()callbacks must be browser-safe and secret-free (they run during discovery AND ship in the client bundle).
| What matters to you | Best choice |
|---|---|
| Existing team, vendor integration or hiring constraint points there | React or Vue |
| Reusable design-system components across host frameworks | Lit |
| Compiled JSX or component-language workflow | Solid or Svelte |
| Progressive enhancement of server-rendered pages | htmx + your backend |
| One component model for sites and apps with calm maintenance | Mado |
mado release # typecheck + vite build + static snapshots + deployment files
mado preview # serve out/ like a real static hostOne command, one artifact (out/). Upload it to a static host after applying
the host-specific routing policy for static, SPA or hybrid routes; see
Deployment.
Canonical docs (English) live in docs/en/.
Build tools can resolve the same ordered source set from the published
@madojs/mado/docs/en/manifest.json asset. The compact framework contract is
also published as @madojs/mado/llms.txt, and the exact package identity and
version are available through @madojs/mado/package.json.
AI-agent entrypoints: AGENTS.md · llms.txt
npm run typecheck
npm run build
npm test
npm run size
npm run package:smokeThe full snapshot + takeover round-trip and the base-path contract
are required CI gates (.github/workflows/ci.yml → static-snapshot),
run under a pinned Playwright-managed Chromium with
MADO_REQUIRE_BROWSER=1 so they never silently skip on PRs.
Read CONTRIBUTING.md. Bug fixes with tests, docs improvements, examples and carefully discussed core changes are welcome. Runtime dependencies are not.
MIT.