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.
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.
-
{#each state.tags as tag}
- {tag} {:else}
- no tags - add one {/each}
{state.tip}% tip is ${state.tip_sum} total ${state.total}
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: changethis.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.
| Concept | React | Svelte 5 | Vue 3 | Fez |
|---|---|---|---|---|
| State | useState, useReducer | $state rune | ref, reactive | this.state.x = y |
| Computed | useMemo | $derived rune | computed | Just use a method |
| Side effects | useEffect | $effect rune | watch, watchEffect | afterRender() |
| Global state | Context, Redux, Zustand | stores | Pinia | this.globalState |
| Two-way binding | onChange + setState | bind:value | v-model | fez:bind |
| Typed props | PropTypes / TS | $props() + TS | defineProps | PROPS schema, coerced at runtime |
| Re-render control | memo, useMemo, keys | {#key} | v-memo | Automatic |
| Build step | JSX + bundler | Svelte compiler | SFC compiler + Vite | None - one script tag |
Things you usually install a library for - Fez ships them
| Need | React | Svelte 5 | Vue 3 | Fez |
|---|---|---|---|---|
| Page navigation | React Router, Next | SvelteKit | Vue Router | Pjax built in - server HTML swapped in place, components survive |
| Enter / leave transitions | framer-motion, react-transition-group | transition: | <Transition> | fez:in, fez:out, fez:transition |
| Reorder / size animation | framer-motion layout | animate:flip | <TransitionGroup> | fez:animate="flip", "height", "size" |
| Fetch with cache | TanStack Query, SWR | load() + fetch | VueUse, Pinia Colada | Fez.fetch() - JSON, cache, in-flight de-dup |
| Component messaging | Context, event bus libs | stores, dispatch | mitt, provide / inject | Fez.pub() / Fez.sub() |
| Scoped styles + nesting | CSS Modules, styled-components, Sass | scoped <style> + preprocessor | scoped <style> + Sass | scoped <style>, SCSS nesting flattened at runtime |
| Async in template | <Suspense> + use() | {#await} | <Suspense> | {#await} / {:then} / {:catch} |
| Form to JSON | react-hook-form, Formik | FormData by hand | v-model + lib | this.formData() |
| Keep DOM across renders | key, memo | {#key} | key, v-memo | fez: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.
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.
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.
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.
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.
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.
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.
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.
Built-in communication
Pub/sub between components and a shared this.globalState that re-renders every subscriber. No store library, no providers.
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.
Single-file components
Style, template and class in one readable HTML file. Load it with <script fez="..."> or define it inline in a <template>.
Tooling without a toolchain
TypeScript definitions ship in the package. fez compile validates a component from the CLI, fez agents prints the LLM reference.
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.
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.
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.
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.
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".
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.
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 server
index.html,fez.jsand a folder of.fezfiles is the whole renderer. Edit a component, reload the window. No Vite config, no HMR plugin, nonode_modulesfor 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 betweenipcRendererand 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.
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.
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.
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.jscovers 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.vuefile. - 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.
-
Add Fez to
<head> -
Create
ui-counter.fezand load it -
Place it anywhere in
<body>Global max:
{globalState.maxCount || 0} Live - global max across all counters:
{globalState.maxCount || 0}
jQuery simplicity. Component-era power.