HTML application framework

Fast applications.
Explicit HTML.

Native Fragments is a small framework for Cloudflare Workers that streams server-rendered HTML, upgrades native navigation, and uses Lit for interactive islands. Modern tooling, very little ceremony.

Cloudflare WorkersLitWeb ComponentsNative APIs

HTMLfirst response
Streamshell, then fragments
Nativelinks and forms
LitSSR + hydration adapter

The working model

One route. Three honest response modes.

The server owns routing and the first render. The browser router asks for the same route as a fragment and swaps only its declared target. Without JavaScript, the anchor simply performs a document navigation.

Direct visit

GET /dashboard Complete document, streamed

Link navigation

GET /dashboard · x-fragment Only the target HTML, streamed

JavaScript off

Native anchor or form The same route still works

Server

Render the application you mean.

Route definitions keep path, metadata, actions, and HTML together. Deferred work starts immediately and fills its own boundary when ready. A slow dependency delays one region—not the whole document.

  • Escaped interpolation by default
  • Abort signals follow the request
  • Error and loading boundaries are visible HTML
import { fragment, html, route } from "@nativefragments/core/server";

const activity = fragment("activity", {
  loading: () => html`<p aria-live="polite">Loading activity…</p>`,
  render: async ({ signal }) =>
    activityFeed(await loadActivity({ signal })),
});

export const dashboard = route("/dashboard", {
  meta: () => ({ title: "Dashboard" }),
  render: (context) => html`
    <h1>Dashboard</h1>
    ${context.defer(activity)}
  `,
  fragments: [activity],
});

Streaming navigation

The next page reveals itself as its HTML arrives.

Fragment navigation uses a framed HTML protocol. The first frame swaps the page immediately; later frames reveal deferred regions out of order. Old tabs that do not speak the protocol receive a safe buffered response.

00 msshellpainted

18 mspage fragmentswapped

112 mssummaryrevealed

384 msartworksrevealed

421 msprovenanceerror boundary

Design constraints

Small core. Deliberate layers.

Native Fragments does not compete with the platform. It defines the contracts the platform is missing for an HTML application, then gets out of the way.

01

HTML is the interface

Routes return escaped HTML. The browser receives useful content before application JavaScript runs.

02

Navigation stays native

Real anchors and forms are the baseline. A small router upgrades them with history, prefetch, and streamed swaps.

03

Interactivity stays local

Use Lit and Web Components for the parts that own state. Server rendering and hydration live in a replaceable adapter.

04

Tooling serves the app

esbuild resolves modern ESM and creates deployable files. It is a fast implementation detail, not a compiler-shaped architecture.

Browser

A router you can hold in your hand.

Starting navigation returns three explicit capabilities. Lifecycle is owned by an AbortSignal; cache invalidation is an application decision; semantic DOM events make integrations observable.

import "@nativefragments/lit/client";
import { startRouter } from "@nativefragments/core/client/router.js";

const lifetime = new AbortController();
const router = startRouter({
  prefetch: "intent",
  signal: lifetime.signal,
});

await router.navigate("/dashboard");
router.prefetch("/settings");
router.invalidate("/dashboard");

Interactive islands

Use Lit where state actually lives.

Components are normal Lit elements. The adapter pins the evolving Lit SSR surface and emits hydratable Declarative Shadow DOM, keeping labs APIs out of application code.

// app-card.js — ordinary Lit
import { LitElement, css, html } from "lit";

class AppCard extends LitElement {
  static styles = css`:host { display: block }`;
  render() { return html`<slot></slot>`; }
}
customElements.define("app-card", AppCard);

// Server route — Lit SSR stays behind one adapter
import { renderLit } from "@nativefragments/lit/server";
import { html } from "lit";

export const card = () =>
  renderLit(html`<app-card>Ready now.</app-card>`);

Build policy

No framework compiler. No build-step theatre.

Source stays standards-based ESM. A tiny esbuild step resolves packages, bundles browser modules, and lets Wrangler run the Worker. You can import from npm without turning the framework into a compiler or forcing every dependency to publish browser-ready bare-specifier graphs.

npm run dev  →  esbuild + wrangler dev --live-reload

Start with the whole stack

From empty directory to streamed HTML.

npm create @nativefragments/app@latest my-app