script src="fez.js"

jQuery simplicity
for the component era.

Add real components to the HTML your server already renders. One script tag - state, a component-aware DOM differ, transitions, typed props, pjax navigation - and no build step, no ownership war over the DOM, no framework rewrite. If you could learn jQuery in an afternoon, you already know Fez.

<script src="https://raw.githubusercontent.com/dux/fez/main/dist/fez.js"></script> Copy
<script src="fez.js"></script> <script fez="ui-clock.fez"></script> <ui-clock city="My place"></ui-clock>
what the browser shows - live, no bundler between the two

two-more-examples

Two more examples, featuring most of Fez

Same setup - one script tag, one .fez file, no build step. Between them: state, form handling, typed props, derived values, two-way binding, keyed loops and enter / leave / reorder animations. Every line below is the whole component.

<script> PROPS = { tags: { type: Array, state: true, default: (raw) => (raw || '').split(/\s*,\s*/).filter(Boolean), }, } onSubmit({ tag }) { this.state.tags = [tag, ...this.state.tags] this.find('form').reset() } remove(tag) { this.state.tags = this.state.tags.filter(t => t != tag) } </script> <form><input name="tag" placeholder="add a tag" required /></form> <ul> {#each state.tags as tag} <li key={tag} fez:animate="flip" fez:in="fly, from=left" fez:out="scale, duration=150"> {tag} <button onclick={() => remove(tag)}>&times;</button> </li> {:else} <li class="empty">no tags - add one</li> {/each} </ul>
<ui-tags tags="state, transitions, no build"></ui-tags>
add and remove - rows fly in, fade out and glide to place
<script> PROPS = { bill: { type: Number, state: true, default: 48 } } init() { this.state.tip = 18 } beforeRender() { const tip = this.state.bill * this.state.tip / 100 this.state.tip_sum = tip.toFixed(2) this.state.total = (this.state.bill + tip).toFixed(2) } </script> <label> bill <input type="number" min="0" step="1" fez:bind="state.bill" /> </label> <input type="range" min="0" max="30" fez:bind="state.tip" style="--pct: {state.tip / 30 * 100}%" /> <p class="out"> <span>{state.tip}% tip is <b>${state.tip_sum}</b></span> <span class="total">total <b>${state.total}</b></span> </p>
<ui-tip bill="48"></ui-tip>
type a bill or drag the slider - both totals recompute on the same pass

the-good-parts

What jQuery got right, Fez keeps

jQuery won because it respected the page you already had and bundled the tools you actually needed into one library that just worked. Fez follows the same logic - one script, the useful parts included - and adds the one thing jQuery never had: real, reactive components.

  • jQueryFez
  • One script tag<script src="jquery.js"> and you were done.<script src="fez.js"> - template compiler, differ, transitions, pjax and fetch included. Nothing to install for the UI.
  • Works on the page you already haveServer-rendered HTML, enhanced in place.The same - <ui-tag> anywhere in your markup, and pjax keeps it alive across navigation.
  • The DOM is the truthYou read and wrote real nodes.Still real nodes - the differ morphs the live DOM, no virtual tree, and touching the DOM yourself is allowed.
  • Learn it in an afternoonOne mental model: $().One mental model: change this.state, the component re-renders. No hooks, runes or dependency arrays.
  • Plays with anythingDropped into any stack.Native custom elements - sits next to React, Rails, Django, Electron, static HTML.

And the part jQuery never solved - components with their own state, scoped style and lifecycle - is the rest of this page.

why-fez

The whole mental model fits in three lines

Change this.state - the component re-renders. The differ touches only what changed and leaves child components alone. An unchanged render is hashed away and costs nothing.

No special syntax. No runes. No hooks.

ConceptReactSvelte 5Vue 3Fez
StateuseState, useReducer$state runeref, reactivethis.state.x = y
ComputeduseMemo$derived runecomputedJust use a method
Side effectsuseEffect$effect runewatch, watchEffectafterRender()
Global stateContext, Redux, ZustandstoresPiniathis.globalState
Two-way bindingonChange + setStatebind:valuev-modelfez:bind
Typed propsPropTypes / TS$props() + TSdefinePropsPROPS schema, coerced at runtime
Re-render controlmemo, useMemo, keys{#key}v-memoAutomatic
Build stepJSX + bundlerSvelte compilerSFC compiler + ViteNone - one script tag

Things you usually install a library for - Fez ships them

NeedReactSvelte 5Vue 3Fez
Page navigationReact Router, NextSvelteKitVue RouterPjax built in - server HTML swapped in place, components survive
Enter / leave transitionsframer-motion, react-transition-grouptransition:<Transition>fez:in, fez:out, fez:transition
Reorder / size animationframer-motion layoutanimate:flip<TransitionGroup>fez:animate="flip", "height", "size"
Fetch with cacheTanStack Query, SWRload() + fetchVueUse, Pinia ColadaFez.fetch() - JSON, cache, in-flight de-dup
Component messagingContext, event bus libsstores, dispatchmitt, provide / injectFez.pub() / Fez.sub()
Scoped styles + nestingCSS Modules, styled-components, Sassscoped <style> + preprocessorscoped <style> + Sassscoped <style>, SCSS nesting flattened at runtime
Async in template<Suspense> + use(){#await}<Suspense>{#await} / {:then} / {:catch}
Form to JSONreact-hook-form, FormikFormData by handv-model + libthis.formData()
Keep DOM across renderskey, memo{#key}key, v-memofez:keep, child components never morphed

what-you-get

One script, the full kit

110KB minified, ~35KB gzipped - and that already includes the template compiler, the DOM differ, the SCSS flattener, transitions, pjax and fetch. Nothing to npm install for the UI - exactly like the script tag you remember.

<ui-clock>

Native custom elements

Standard browser API. Create a tag, put it anywhere in your HTML - server-rendered, injected, or typed into dev tools - and it just works.

morph

Component-aware differ

Real-DOM morph, no virtual DOM. Renders are hashed so a no-op costs nothing; child components are repositioned, never re-rendered; focus, scroll, input values and running CSS animations survive.

fez:in / fez:out

Transitions built in

fade, fly, slide, scale, pop, blur, flip, rotate, draw - plus fez:animate="flip" for list reorders and "height" for content that grows. Web Animations API, honours prefers-reduced-motion.

PROPS = { ... }

Typed props

Declare a schema and attribute strings arrive in this.props as numbers, booleans, arrays, dates - with defaults, required and enum. No parseInt by hand.

<main class="pjax">

Pjax navigation

Server-rendered pages are fetched and morphed in place. History preserved, assets not re-parsed, components on the page keep their state. Partial swaps with pjax-target.

this.state.a.b = c

Deep reactive state

Nested writes trigger a render, multiple writes in one tick batch into a single frame. No immutable update dance, no dependency arrays.

<style>

Scoped SCSS-style CSS

Nested rules and & flattened to plain CSS at runtime - works on browsers older than native nesting. <style global> when you mean document-wide.

Fez.pub / globalState

Built-in communication

Pub/sub between components and a shared this.globalState that re-renders every subscriber. No store library, no providers.

onDestroy()

Auto-cleanup

Timers, listeners and subscriptions registered through the component are torn down when it leaves the DOM. Lifecycle: init, onMount, beforeRender, afterRender, onPropsChange, onDestroy.

.fez

Single-file components

Style, template and class in one readable HTML file. Load it with <script fez="..."> or define it inline in a <template>.

fez.d.ts

Tooling without a toolchain

TypeScript definitions ship in the package. fez compile validates a component from the CLI, fez agents prints the LLM reference.

Fez.fetch()

Fetch, forms, slots

JSON fetch with in-memory cache and in-flight de-dup, this.formData() to serialize a form, <slot /> with listener preservation and <slot unwrap />.

llm-friendly

Why LLMs and AI agents love Fez

Less surface to learn, less text to write, a deterministic way to check the result. A coding assistant ships a working Fez component in one pass - and you can read the whole thing in one screen.

fewer tokens

One file, no boilerplate

A component is one .fez file - style, template and class - instead of a component file plus CSS module plus types plus hooks plus imports. Shorter output means cheaper, faster generations and a diff a human can review in a minute.

one mental model

Nothing to get subtly wrong

No hooks order, no dependency arrays, no rune semantics that changed between versions, no "is this reactive?" guesswork. this.state.x = y re-renders. The model cannot hallucinate an API that is not there.

plain JS + real DOM

Trained on it already

Vanilla JavaScript, native custom elements, the live DOM. That is the code models have seen the most of - the prior is strong and stays valid because the Fez surface does not churn every major version.

fez compile

Deterministic check, no browser

fez compile my-comp.fez validates JavaScript and template in milliseconds - the agent fixes errors before anyone opens a page. No bundler to start, no dev server to wait on, no "why doesn't Vite see my file".

fez agents

The whole reference fits in context

fez agents --init drops a pointer into your project's AGENTS.md; fez agents prints the full reference. One source of truth, small enough to read in full, so the assistant is not guessing from half-remembered docs.

robust by default

Whole bug classes removed

Typed PROPS coerce attributes, timers and listeners clean themselves up, the differ keeps focus and input state, transitions need no library. Fewer places for a generated component to be subtly broken - fewer review rounds.

llm-verdict

Coding assistants pick Fez for Electron

When asked to scaffold a new Electron desktop app, the assistant proposes Fez on its own, as the cleaner alternative to React, Svelte and Vue - unprompted. The reasons it gives are on the right.

Assistants read AGENTS.md and write correct components in one pass: fez agents prints the reference, fez compile validates the result.

  • One Chromium targetElectron's renderer is a known, modern Chromium - nothing legacy to polyfill for, so the "needs a compiler" argument for React/Svelte/Vue evaporates. Custom elements and the Web Animations API are simply there.
  • No bundler, no dev serverindex.html, fez.js and a folder of .fez files is the whole renderer. Edit a component, reload the window. No Vite config, no HMR plugin, no node_modules for the UI.
  • Vanilla DOM all the way downThe preload / IPC bridge hands you plain objects; a Fez component is a plain class with this.state. No adapter, no store layer between ipcRenderer and the view.
  • Small, predictable runtime~35KB gzipped for the whole framework, no virtual DOM, real DOM morphing - focus, scroll and input state survive renders, which desktop UIs care about more than marketing pages do.
  • Readable for the next person - or modelSingle-file components in HTML and a template syntax that fits on one screen of AGENTS.md. Assistants prefer it because there is less to get wrong.

where-it-fits

Real components in the app you already have

Fez is designed for one specific job: adding real components to a server-rendered application - without a build step, without taking ownership of the DOM, and without rewriting the application around a framework.

Svelte and Vue are excellent at building applications that they render end to end; that is a different job, and their design assumes they own the page. Turbo + Stimulus address the same server-rendered case as Fez, but with a different split between what the framework provides and what you write by hand. The comparison below is limited to that one scenario.

<script src="fez.js">

No build step

The template compiler, the differ and the SCSS flattener all run in the browser. Drop one script tag next to the HTML your server already returns and <ui-tag> starts working. No bundler, no dev server, no node_modules for the UI.

morph, not mount

No ownership of the DOM

Real-DOM morphing, no virtual tree. Server HTML stays the source of truth, components may read and write live nodes directly, and third-party widgets survive a render behind fez:keep.

one tag at a time

No framework rewrite

Adoption can stop at a single component. Native custom elements sit next to Rails, Django, Laravel, PHP or plain HTML, and pjax keeps them alive across navigation.

Compared with Turbo + Stimulus

  • Templates in addition to controllers. Stimulus provides a controller and leaves DOM updates to hand-written code. Fez adds {#if}, {#each}, {#await}, scoped <style>, transitions and typed props in the same file.
  • A component-aware differ. Turbo replaces a frame. Fez morphs: a keyed component survives the page swap with its state intact, while one whose server markup changed gets a clean init(), so there is no manual re-syncing step.
  • A single script. Turbo, Stimulus, an import map and a build pipeline are separate pieces; fez.js covers the same ground in one file.

Compared with Svelte + Vue

  • Svelte has no runtime compiler. Its compiler is an essential part of the framework, so every component goes through a build; adding a script tag to an existing page is not a supported mode.
  • Vue can run from a CDN, with trade-offs. The in-DOM template build does not support the single-file component: no scoped <style> block, no .vue file.
  • Both render from a mount point. They build the tree beneath it and expect to own it, so on a server-rendered page the templates are ported first. Fez enhances the markup that is already there.

The trade-off, and where it stops applying

A render rebuilds the component's whole template, so its cost tracks template size rather than how much changed. That is a deliberate trade for a single mental model that holds for the large majority of components - change this.state, the component re-renders - with no hooks, runes, dependency arrays or memo tuning.

When a component genuinely needs raw DOM throughput - a 10k-row table, a live log, a spreadsheet - that one component can opt out of this.state rather than out of Fez. Because Fez does not own the page, the live DOM remains available: wall off a subtree with fez:keep, keep data in plain instance fields, or skip the template and paint the DOM directly in onMount(). 10k rows render in roughly 9ms and cell updates run at vanilla speed, while lifecycle, typed props, scoped style and auto-cleanup still apply. Nothing else on the page changes.

ui-counter

A counter in three steps

Local state per instance, one shared global value across all of them, scoped style - in a single file.

  1. Add Fez to <head>

    <script src="https://raw.githubusercontent.com/dux/fez/main/dist/fez.js"></script>
  2. Create ui-counter.fez and load it

    <script fez="./ui-counter.fez"></script>
  3. Place it anywhere in <body>

    <p>Global max: <fez-inline>{globalState.maxCount || 0}</fez-inline></p> <ui-counter></ui-counter> <ui-counter start="1"></ui-counter> <ui-counter start="2"></ui-counter>

    Live - global max across all counters: {globalState.maxCount || 0}

jQuery simplicity. Component-era power.