Control
Keep boundaries explicit
State, rendering, messages, and routing remain separate packages with ordinary JavaScript contracts.
Framework-independent TypeScript packages for progressive enhancement, explicit application structure, and incremental adoption in new or existing server-rendered web apps.
Why White Label
Use the web platform directly, adopt only the primitives a feature needs, and keep meaningful HTML as the foundation instead of hiding the application behind a framework runtime.
Control
State, rendering, messages, and routing remain separate packages with ordinary JavaScript contracts.
Platform
Real links, native events, the DOM, History API, Web Request/Response, Node.js, and TypeScript stay visible and usable.
Adoption
Use one package or compose the stack. Static HTML, server-rendered pages, and request-scoped Node.js functions can use the same focused primitives.
Where White Label fits
White Label is most differentiated when the web page already matters as HTML and JavaScript should enhance a deliberate boundary rather than become the only way the application works.
Existing applications
Adopt server-rendered DOM, observable state, events, or scoped routing without replacing the backend, templates, or unrelated page behavior.
Public + commerce content
Progressive enhancement fits storefronts, CMS/content sites, account areas, search/refinement flows, and documentation where accessibility, crawlability, and resilient navigation matter.
Small explicit systems
Agencies, multi-client teams, and framework-independent projects can share clear TypeScript boundaries while choosing their own templates, networking, persistence, and deployment platform.
Deeply client-owned editors, design tools, collaborative workspaces, games, and other highly stateful SPA-style products may benefit more from a full component framework and its shared conventions.
Architecture
Each package has a focused responsibility. Use one by itself or compose them when a feature needs the full flow.
URL → application intent.
Intent → decoupled events.
Events → observable state.
State → rendered output.
These are responsibilities, not mandatory layers. A feature that only needs state can use Model by itself.
Learn by doing
Each card shows the HTML and TypeScript needed to reproduce the live interaction beside it.
Model
Store state, listen once, then update it synchronously.
HTML<div class="live-demo" data-live-demo="model">
<span class="live-demo__label">Current state</span>
<strong class="live-demo__value" data-demo-output role="status" aria-live="polite" aria-atomic="true">0</strong>
<button class="button" type="button" data-demo-action="increment">Increment count</button>
</div>TypeScriptimport {Model} from 'white-label-model';
const demo = document.querySelector<HTMLElement>('[data-live-demo="model"]');
if (!demo) {throw new Error('Missing Model demo');}
const output = demo.querySelector<HTMLElement>('[data-demo-output]')!;
const button = demo.querySelector<HTMLButtonElement>('[data-demo-action="increment"]')!;
const model = new Model({count: 0}).initialize();
const render = (event?: CustomEvent<{count: number}>): void => {
const state = event?.detail ?? model.get();
output.textContent = String(state.count);
};
const increment = (): void => {
model.update({count: model.get().count + 1});
};
model.addEventListener('change', render);
button.addEventListener('click', increment);
render();Open API indexTry it
View
Give View a model and a template. Model changes trigger rendering.
HTML<div data-view-demo></div>TypeScriptimport {Model} from 'white-label-model';
import View from 'white-label-view';
import {html} from 'white-label-view/html';
const mount = document.querySelector<HTMLElement>('[data-view-demo]');
if (!mount) {throw new Error('Missing View demo');}
const model = new Model({name: 'Ada'});
class ExampleView extends View {
handleChange = (): void => {
model.update({name: model.get().name === 'Ada' ? 'Grace' : 'Ada'});
};
override addListeners() {
this.delegated.on('click', '[data-demo-action="change"]', this.handleChange);
return this;
}
}
new ExampleView({
parentElement: mount,
model,
template(data) {
const state = data as {name: string};
return html`<div class="live-demo" data-live-demo="view">
<span class="live-demo__label">Rendered output</span>
<p class="live-demo__value" data-view-mount role="status" aria-live="polite" aria-atomic="true">Hello, ${state.name}</p>
<button class="button" type="button" data-demo-action="change">Change model</button>
</div>`;
}
}).initialize();Open API indexTry it
Mediator
Subscribers react to named events without the publisher knowing who they are.
HTML<div class="live-demo" data-live-demo="mediator">
<span class="live-demo__label">Last message</span>
<strong class="live-demo__value" data-demo-output role="status" aria-live="polite" aria-atomic="true">Waiting…</strong>
<button class="button" type="button" data-demo-action="publish">Publish event</button>
</div>TypeScriptimport Mediator from 'white-label-mediator';
const demo = document.querySelector<HTMLElement>('[data-live-demo="mediator"]');
if (!demo) {throw new Error('Missing Mediator demo');}
const output = demo.querySelector<HTMLElement>('[data-demo-output]')!;
const button = demo.querySelector<HTMLButtonElement>('[data-demo-action="publish"]')!;
const mediator = new Mediator<{message: string}>().initialize();
let sequence = 0;
const receive = (event: CustomEvent<string>): void => {
output.textContent = event.detail;
};
const publish = (): void => {
sequence += 1;
mediator.dispatchEvent(new CustomEvent('message', {
detail: `Hello #${sequence}`
}));
};
mediator.addEventListener('message', receive);
button.addEventListener('click', publish);Open API indexTry it
Router
Define a route, then enhance real links instead of replacing them.
HTML<div class="live-demo" data-live-demo="router">
<span class="live-demo__label">Current route</span>
<strong class="live-demo__value" data-router-demo-output role="status" aria-live="polite" aria-atomic="true">Home</strong>
<nav class="live-demo__links" aria-label="Router live demo">
<a href="/?demo=home#examples" data-pushstate data-router-demo-link="home">Home</a>
<a href="/?demo=products#examples" data-pushstate data-router-demo-link="products">Products</a>
</nav>
</div>TypeScriptimport Router from 'white-label-router';
const demo = document.querySelector<HTMLElement>('[data-live-demo="router"]');
if (!demo) {throw new Error('Missing Router demo');}
const output = demo.querySelector<HTMLElement>('[data-router-demo-output]')!;
const links = [...demo.querySelectorAll<HTMLAnchorElement>('[data-router-demo-link]')];
const route = (_scope: Element | null, location: Router.Location): void => {
const selected = location.data.query.demo === 'products' ? 'products' : 'home';
output.textContent = selected === 'products' ? 'Products' : 'Home';
for (const link of links) {
if (link.dataset.routerDemoLink === selected) {
link.setAttribute('aria-current', 'page');
} else {
link.removeAttribute('aria-current');
}
}
};
const router = new Router();
router.scope = demo;
router.routes = {'/': route, defaultRoute: route};
router.initialize();Open API indexTry it
API reference
The homepage stays focused on learning the architecture. The API index is optimized for scanning public signatures and jumping directly into the authoritative package documentation.
Observable state
Object, array, and Map state with synchronous observation and optional runtime validation.
Rendering + lifecycle
Browser and DOM-free server rendering, adopted DOM, model binding, delegated events, first-party tagged HTML, renderer choice, ownership, and cleanup.
Application events
Synchronous standards-based application events without coupling publishers to subscribers.
URL → application intent
Progressive browser routing with scoped link interception plus the same matching contract for server dispatch.
Project creation
Create one canonical HTML-first TypeScript project with tagged templates plus a provider-neutral Request/Response handler example.
Get started
Start with a generated application, or install only the primitives your existing project needs.
npx generator-white-label create my-projectThe generator creates one TypeScript starter using White Label View's first-party tagged HTML templates and includes the provider-neutral server handler example.
npm install white-label-model white-label-view
npm install white-label-mediator white-label-routerEvery runtime package works independently. Add another piece only when the feature needs its responsibility.