Small pieces.
Complete applications.

Framework-independent TypeScript packages for progressive enhancement, explicit application structure, and incremental adoption in new or existing server-rendered web apps.

Runtime
Browser + Node.js
Architecture
Explicit composition
Rendering
Existing DOM, tagged HTML, or application-owned renderers
Approach
HTML first, JS enhances

Why White Label

Own the application architecture.

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

Keep boundaries explicit

State, rendering, messages, and routing remain separate packages with ordinary JavaScript contracts.

Platform

Stay close to the web

Real links, native events, the DOM, History API, Web Request/Response, Node.js, and TypeScript stay visible and usable.

Adoption

Take only what you need

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

Add structure without a framework takeover.

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

Modernize one region at a time

Adopt server-rendered DOM, observable state, events, or scoped routing without replacing the backend, templates, or unrelated page behavior.

Read the incremental adoption guide

Public + commerce content

Keep real HTML and URLs authoritative

Progressive enhancement fits storefronts, CMS/content sites, account areas, search/refinement flows, and documentation where accessibility, crawlability, and resilient navigation matter.

Small explicit systems

Use primitives instead of a framework stack

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

One job per piece.

Each package has a focused responsibility. Use one by itself or compose them when a feature needs the full flow.

  1. 01Router

    URL → application intent.

  2. 02Mediator

    Intent → decoupled events.

  3. 03Model

    Events → observable state.

  4. 04View

    State → rendered output.

These are responsibilities, not mandatory layers. A feature that only needs state can use Model by itself.

Learn by doing

Four ideas. Four complete examples.

Each card shows the HTML and TypeScript needed to reproduce the live interaction beside it.

Model

Observe state changes

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>
TypeScript
import {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 index

Try it

View

Render from a model

Give View a model and a template. Model changes trigger rendering.

HTML
<div data-view-demo></div>
TypeScript
import {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 index

Try it

Mediator

Publish without coupling

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>
TypeScript
import 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 index

Try it

Router

Turn a URL into intent

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>
TypeScript
import 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 index

Try it

API reference

API index, on its own page.

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.

Browse the API index

Observable state

white-label-model

Detailed documentation

Object, array, and Map state with synchronous observation and optional runtime validation.

Open API

Rendering + lifecycle

white-label-view

Detailed documentation

Browser and DOM-free server rendering, adopted DOM, model binding, delegated events, first-party tagged HTML, renderer choice, ownership, and cleanup.

Open API

Application events

white-label-mediator

Detailed documentation

Synchronous standards-based application events without coupling publishers to subscribers.

Open API

URL → application intent

white-label-router

Detailed documentation

Progressive browser routing with scoped link interception plus the same matching contract for server dispatch.

Open API

Project creation

generator-white-label

Detailed documentation

Create one canonical HTML-first TypeScript project with tagged templates plus a provider-neutral Request/Response handler example.

Open API

Get started

Choose your entry point.

Start with a generated application, or install only the primitives your existing project needs.

New project

Generate the starter

npx generator-white-label create my-project

The generator creates one TypeScript starter using White Label View's first-party tagged HTML templates and includes the provider-neutral server handler example.

Existing project

Install only what you need

npm install white-label-model white-label-view
npm install white-label-mediator white-label-router

Every runtime package works independently. Add another piece only when the feature needs its responsibility.

See the server-rendered integration pattern