JST

JavaScript Templates (JST): reactive components in plain HTML, with JavaScript itself as the templating language. No build step.

It's just Web Components under the hood. Every <jst-*> is a real custom element from the platform's own customElements.define. JST renders them; what you ship is standard custom elements.

●This is currently an experimental preview ●This framework is still under active development, improvement, and experimentation ●New features are being explored to gain parity with the major players  

This is JST

The button below is a JST component, defined and rendered by the same jst.js this page loads. View source: it's all here, nothing built.

Live

It's a real custom element: open DevTools and it's <jst-demo-counter>; type document.querySelector('jst-demo-counter').count = 9 in the console and watch it re-render. Local state via element properties, no framework state object, no hooks.

Source

<script type="jst" name="jst-demo-counter" attributes="count">
  <button class="demo-btn" onclick="el.count = (el.count || 0) + 1">
    Clicked $(count || 0) time$(count === 1 ? '' : 's')
  </button>
</script>

<jst-demo-counter count="0"></jst-demo-counter>
Hover over the codeHover over parts of the code snippet to get a detailed explanation.

How does it work?

JST is a thin layer over the Web Components platform. A <script type="jst"> tag is compiled into a class and registered with customElements.define(), so your <jst-*> tags are genuine custom elements, not a framework's virtual stand-ins. You write them in ordinary HTML with JavaScript as the templating language: no build-time compiler, no bundler, no JSX, no virtual DOM.

Because they're real custom elements, they behave like the platform: they show up in DevTools as <jst-counter>, you set data with plain properties (el.count = 5), you listen with addEventListener, they work inside any framework or none, and they upgrade automatically when their definition arrives, even from the server. JST adds rendering and ergonomics; the component is the browser's.

The hole it fills

JST started from a question: what would a front-end SSR component look like? Could the server send down components, like HTMX, but with real client-side interactivity, like Alpine, and without a build step, a virtual DOM, or signals?

ToolStrengthThe gap JST targets
HTMXServer returns HTML; hypermedia-driven.Optimised for server-driven interactions; reusable client component definitions are outside its core model.
AlpineSprinkle client interactivity into existing HTML.Local behaviour is the primary abstraction; JST instead targets streamed custom-element definitions with explicit attributes and events.
React / VuePowerful component models.Build step, virtual DOM, framework runtime, JSX/SFC tooling.
JSTComponents in plain HTML that the server can stream down (a fetched fragment can define and use a component; it auto-registers), with full client interactivity, no build, and no virtual DOM or signals. It rides the platform: custom elements, properties, events, DOM morphing.

We tested that claim: 126 canonical examples from HTMX, Alpine, Vue, React, Lit, fixi, Svelte, Solid, and Angular, rebuilt in JST. 103 are currently classified as idiomatic matches and 23 use a documented workaround. Those classifications are an author assessment; the verifier separately checks every page's readiness, errors, rendered output, and an interaction where the example provides one.

Hypertext as your API, not JSON

A typical web app is a JSON API plus a separate client that turns that JSON back into HTML: every screen defined twice and kept in sync by hand. JST takes the older idea (HATEOAS): a response is the UI, HTML that carries its own next actions.

The part HTMX can't do: a JST response is HTML containing real <script type="jst"> components, so it brings its own front-end interactivity. The browser registers and runs the JS on arrival. You get HTMX's "the server just returns HTML" simplicity and real client-side behaviour in the very same response. No JSON contract, no parallel client app, no duplicated views.

Trust boundary: a streamed <script type="jst"> template is executable JavaScript. Only load and auto-register component definitions from trusted sources. Ordinary interpolated data is escaped; that does not make an untrusted template safe.

You do not need a client router to get there. Use ordinary resourceful URLs and full-page navigation by default: /orders/4471, /orders/4471/edit, POST /orders/4471/items. Reach for fragment swaps only where they improve a workflow: inline editing, live validation, expanding rows, or streaming a component definition the page did not know yet.

And you may not need a backend at all. JST is just static files and the browser: no API, no server-side code, no build. The same components a server could stream down run perfectly well entirely on the client, so a whole app can be a folder of static files. (This site is exactly that.)

Open the HATEOAS service-worker demo →

Philosophy

Examples

Everything below runs live in an isolated frame. Use Open standalone ↗ on any example to interact with it on its own page, inspect it in DevTools, or read its source — it's plain HTML and jst.js, nothing built.

Kanban board

A three-level component tree (board → column → card), native drag & drop, a modal editor, composable filters, and an insights panel whose component definition is fetched from the "server" at runtime.

Attributes down, events up: the page owns the list, components are dumb renderers, and interpolation is escaped by default.

Component gallery

A CSS-first component set: layout primitives, a stack of everyday patterns that are mostly just platform HTML + CSS variables, and a handful of real JST custom elements where the platform has no equivalent. Every one is live below in its own frame — open it standalone, read its source, interact with it — and the whole gallery re-skins to another framework's look from the single dropdown.

Slots & composition

Light-DOM slots: a component projects the author's children via $(slot()) and named $(slot('name', 'fallback')).

VS Code tooling

Because JST is plain HTML + JavaScript, the editor experience rides existing tools rather than reinventing them. The extension adds three tiers, all tested headlessly.

Syntax highlighting

TextMate injection grammars highlight $(…), $ … and .prop/on<event> inside <script type="jst">, delegating the embedded JS to the editor's own source.js.

Diagnostics

Runs the real JST compiler over each template, so an editor squiggle is exactly what the runtime would reject. Embedded-JS syntax errors are caught by V8 itself.

Language server

Cross-file go-to-definition from a <my-card> tag to its <script type="jst" name="my-card">, hover with a component's attributes, completion, and document symbols.

<!-- the grammar highlights these as embedded JS -->
<script type="jst" name="todo-item" attributes="item">
  <li onclick="el.emit('toggle', item)">$(item.title)</li>
</script>
Hover over the codeHover over parts of the code snippet to get a detailed explanation.