Static sites · Server-side rendering · Islands · SPAs

The best of every stack, in one framework

Lumiose is a web framework for everything from static sites and SPAs to full-stack apps: TSX with scoped CSS built in, signals, and 0 kB of JavaScript until a page needs it.

npm create lumiose@latest my-app

0.4.0 · pre-release

  • 1.08×vanilla JS on js-framework-benchmark
  • 0 kBJavaScript on server-rendered pages
  • 6.4 kBruntime and app, compressed

Compare the stacks

Feels like React. Runs like vanilla JS.

Components, styles, routing and the server in one project, measured against the stacks you already know.

Lumiose compared with other web framework stacks
StackFull-stack layerStylesJS on a server pageClient speedServer speed
LumioseBuilt inBuilt in0 kB1.08×619 renders/s1,323 req/s
React + Next.jsSeparate projectPick a library119 kB1.54×185 renders/s437 req/s
React RouterBuilt inPick a library101 kB1.54×185 renders/s683 req/s
PreactNone officialPick a libraryNo full-stack layer1.52×208 renders/sNo full-stack layer
AstroBuilt inBuilt in0 kBDepends on the UI framework206 renders/s1,247 req/s
Svelte + SvelteKitSeparate projectBuilt in38 kB1.17×505 renders/s1,067 req/s
Vue + NuxtSeparate projectBuilt in76 kB1.25×204 renders/s608 req/s
Solid + SolidStartSeparate projectPick a library33 kB1.11×580 renders/s999 req/s
Remix 3Built inBuilt in42 kB1.57×44 renders/s823 req/s

JS on a server page: what the fullstack comparison's issue tracker loads for its home page, brotli (2026-10-12 · Apple M1 Max (10 cores) · macOS 27.0.1 · Node 26.8.1). Client speed: js-framework-benchmark's weighted factor, where 1.00 is the fastest framework in the run and vanilla JS scores 1.00. Server speed: renders per second of the SSR benchmark's 5,000-component page to an HTML string (2026-10-12, Node v26.8.1, Apple M1 Max (10 cores, 32 GB), darwin 27.0.0 arm64), then requests per second on the fullstack comparison's home page (2026-10-12 · Apple M1 Max (10 cores) · macOS 27.0.1 · Node 26.8.1). Both React rows use React's client and rendering entries. Rows after Lumiose are ordered by npm downloads of each stack's main package (2026-09-10 to 2026-10-09). Method and every run: benchmarks.

Astro's interactive islands need a second UI framework.

Why it feels light

Four things, each one click deep

Familiar to write

A .stsx file is React-like JSX with scoped CSS built in: one <style> at the end of the file serves every component in it, typed by stock TypeScript. Edit the sample: it compiles in your browser.

Open the playground
Counter sample
App.stsx
import { signal } from "lumiose";

const gap = 8;
const radius = { pill: "999px" };

export default function Counter() {
  const count = signal(0);

  return (
    <div class="counter">
      <button class={{ pill: true, hot: count() > 9 }} onClick={() => count.update((n) => n + 1)}>
        Clicked {count()} times
      </button>
      <Reset onReset={() => count.set(0)} />
    </div>
  );
}

function Reset(props: { onReset(): void }) {
  return (
    <button class="pill ghost" onClick={props.onReset}>
      Reset
    </button>
  );
}

// One block at the end of the file styles both components.
<style>
  .counter {
    display: flex;
    gap: calc($gap * 1px);
  }
  .pill {
    padding: calc($gap * 1px) calc($gap * 2px);
    border: 2px solid #5546ff;
    border-radius: $radius.pill;
    background: #5546ff;
    color: white;
    font: inherit;
  }
  .ghost {
    background: transparent;
    color: inherit;
  }
  .hot {
    outline: 2px solid tomato;
  }
</style>
App.js
import { signal } from "lumiose";
import { template, bind, toggleClass, insert, createComponent, delegateEvents } from "lumiose/dom";
import "/src/App.stsx?lumiose&type=style&lang.css";
const _t0 = /* @__PURE__ */ template(`<div class=counter data-l-rdjmzp3m><button class=pill data-l-rdjmzp3m>Clicked <!> times`);
const _t1 = /* @__PURE__ */ template(`<button class="pill ghost" data-l-rdjmzp3m>Reset`);
const _a0 = (el, v) => toggleClass(el, "hot", v);
const gap = 8;
const radius = { pill: "999px" };
export default function Counter() {
	const count = signal(0);
	const _el = _t0(), _button = _el.firstChild, _marker = _button.firstChild.nextSibling;
	bind(_button, () => count() > 9, _a0);
	_button.$$click = () => count.update((n) => n + 1);
	insert(_button, count, _marker);
	insert(_el, createComponent(Reset, { onReset: () => count.set(0) }));
	return _el;
}
function Reset(props) {
	const _el2 = _t1();
	_el2.$$click = props.onReset;
	return _el2;
}
delegateEvents(["click"]);
App.css
.counter[data-l-rdjmzp3m] {
  gap: 8px;
  display: flex;
}

.pill[data-l-rdjmzp3m] {
  color: #fff;
  font: inherit;
  background: #5546ff;
  border: 2px solid #5546ff;
  border-radius: 999px;
  padding: 8px 16px;
}

.ghost[data-l-rdjmzp3m] {
  color: inherit;
  background: none;
}

.hot[data-l-rdjmzp3m] {
  outline: 2px solid tomato;
}

Light and fast, measured

The same issue tracker built three ways: the JavaScript a server-rendered page loads.

JS on a server pagekB, brotli, lower is better
  1. Lumiose0
  2. Astro0
  3. SolidStart33
  4. SvelteKit38
  5. Remix 342
  6. Nuxt76
  7. React Router101
  8. Next.js119
All benchmarks

Explicit, for you and your agent

No hidden machinery: client boundaries where you write them, every diagnostic with a LUMxxxx code and a fix, and the docs in the package, served to your agent over MCP.

terminal
$ lumiose check
src/Counter.stsx:5:9 - warning LUM2001: `doubled` is computed once from a reactive value, so the JSX that uses it never updates

5 |   const doubled = count() * 2;
  |         ~~~~~~~~~~~~~~~~~~~~~

  help: Derive it lazily: `const x = () => …` (called as `x()` in JSX) or `computed(() => …)`.
  docs: lumiose docs errors/LUM2001

Found 0 errors, 1 warning in 1 file.
Docs and MCP for agents

Start

Enlighten your next project.