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.
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.
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.
<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>
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.
<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.
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?
| Tool | Strength | The gap JST targets |
|---|---|---|
| HTMX | Server returns HTML; hypermedia-driven. | Optimised for server-driven interactions; reusable client component definitions are outside its core model. |
| Alpine | Sprinkle client interactivity into existing HTML. | Local behaviour is the primary abstraction; JST instead targets streamed custom-element definitions with explicit attributes and events. |
| React / Vue | Powerful component models. | Build step, virtual DOM, framework runtime, JSX/SFC tooling. |
| JST | Components 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.
<script type="jst"> in plain HTML. The runtime compiler handles them in the browser; precompilation remains available for production and strict CSP.$(expr) to interpolate, $ if / $ forEach for control flow. No new DSL: if you know JavaScript, you know JST.<jst-*> as a real custom element via customElements.define. Data in as properties, actions out as bubbling CustomEvents, DOM patched by morphing: no virtual DOM, no signals, no shadow-DOM lock-in.trustedHTML().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.
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.
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.
The same grid lives on its own at the full component gallery page. The layout primitives page runs with zero JavaScript.
Light-DOM slots: a component projects the author's children via
$(slot()) and named $(slot('name', 'fallback')).
Because JST is plain HTML + JavaScript, the editor experience rides existing tools rather than reinventing them. The extension adds three tiers, all tested headlessly.
TextMate injection grammars highlight $(…), $ … and .prop/on<event> inside <script type="jst">, delegating the embedded JS to the editor's own source.js.
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.
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>