Fez
Modern (Svelte like) web components with reactive state, scoped styles, zero dependencies and no compile.
Install / Use
/learn @dux/FezREADME
FEZ - Custom DOM Elements
Check the Demo site https://dux.github.io/fez/
FEZ is a small library (49KB minified, ~18KB gzipped) that allows writing of Custom DOM elements in a clean and easy-to-understand way.
It uses
- Goober to enable runtime SCSS (similar to styled components)
- Custom component-aware DOM differ to morph DOM from one state to another (as React or Stimulus/Turbo does it), with hash-based render skipping for zero-cost no-op renders
It uses minimal abstraction. You will learn to use it in 15 minutes, just look at examples, it includes all you need to know.
How to install
<script src="https://dux.github.io/fez/dist/fez.js"></script>
CLI Tools
Fez provides command-line tools for development:
# Compile and validate Fez components
bunx @dinoreic/fez compile demo/fez/*.fez
Or install globally:
bun add -g @dinoreic/fez
fez compile my-component.fez
Why Fez is Simpler
| 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 |
| Re-render control | memo, useMemo, keys | {#key} | v-memo | Automatic |
No special syntax. No runes. No hooks. No compiler magic. Just plain JavaScript:
class MyComponent extends FezBase {
init() {
this.state.count = 0; // reactive - nested changes tracked too
}
increment() {
this.state.count++; // triggers re-render automatically
}
get doubled() {
// computed value - just a getter
return this.state.count * 2;
}
}
The whole mental model:
- Change
this.state-> component re-renders - Component-aware differ updates only what changed (child components preserved automatically)
- Hash-based skip avoids DOM work entirely when template output is identical
Little more details
Uses DOM as a source of truth and tries to be as close to vanilla JS as possible. There is nothing to learn or "fight", or overload or "monkey patch" or anything. It just works.
Although fastest, Modifying DOM state directly in React / Vue / etc. is considered an anti-pattern. For Fez this is just fine if you want to do it. Fez basically modifies DOM, you just have a few helpers to help you do it.
It replaces modern JS frameworks by using native Autonomous Custom Elements to create new HTML tags. This has been supported for years in all major browsers.
This article, Web Components Will Replace Your Frontend Framework, is from 2019. Join the future, ditch React, Angular and other never defined, always "evolving" monstrosities. Vanilla is the way :)
There is no some "internal state" that is by some magic reflected to DOM. No! All methods Fez use to manipulate DOM are just helpers around native DOM interface. Work on DOM raw, use built in node builder or full template mapping with DOM morphing.
How it works
- define your custom component -
Fez('ui-foo', class UiFoo extends FezBase) - add HTML -
<ui-foo bar="baz" id="node1"></ui-foo>- lib will call
node1.fez.init()when node is added to DOM and connect your component to dom. - use
Fezhelper methods, or do all by yourself, all good.
- lib will call
That is all.
Template Syntax (Svelte-like)
Fez uses a Svelte-inspired template syntax with single braces { } for expressions and block directives.
Expressions
<!-- Simple expression -->
<div>{state.name}</div>
<!-- Expressions in attributes (automatically quoted) -->
<input value={state.text} class={state.active ? 'active' : ''} />
<!-- Raw HTML (unescaped) -->
<div>{@html state.htmlContent}</div>
<!-- JSON debug output -->
{@json state.data}
Conditionals
{#if state.isLoggedIn}
<p>Welcome, {state.username}!</p>
{:else if state.isGuest}
<p>Hello, Guest!</p>
{:else}
<p>Please log in</p>
{/if}
<!-- Unless (opposite of if) -->
<!-- renders if state.items is null, undefined, empty array, or empty object -->
{#unless state.items}
<p>No items found</p>
{/unless}
Truthiness rules for #if, #unless, and :else if:
null,undefined,false,0,""→ falsy[](empty array) → falsy{}(empty object) → falsy- Non-empty arrays, non-empty objects, and other truthy values → truthy
Loops
<!-- Each loop with implicit index 'i' -->
{#each state.items as item}
<li>{item.name} (index: {i})</li>
{/each}
<!-- Each loop with explicit index -->
{#each state.items as item, index}
<li>{index}: {item.name}</li>
{/each}
<!-- For loop syntax -->
{#for item in state.items}
<li>{item}</li>
{/for}
<!-- For loop with index -->
{#for item, idx in state.items}
<li>{idx}: {item}</li>
{/for}
<!-- Object iteration (2-param = key/value pairs) -->
{#for key, val in state.config}
<div>{key} = {val}</div>
{/for}
<!-- Object iteration with index (3 params) -->
{#each state.config as key, value, index}
<div>{index}. {key} = {value}</div>
{/each}
<!-- Nested values stay intact (not deconstructed) -->
{#for key, user in state.users}
<div>{key}: {user.name}</div>
{/for}
<!-- Empty list fallback with :else -->
{#each state.items as item}
<li>{item}</li>
{:else}
<li>No items found</li>
{/each}
<!-- :else also works with #for -->
{#for item in state.items}
<span>{item}</span>
{:else}
<p>List is empty</p>
{/for}
<!-- Child components in loops - automatically optimized -->
<!-- Use :prop="expr" to pass objects/functions (not just strings) -->
{#each state.users as user}
<user-card :user="user" />
{/each}
Loop behavior:
- null/undefined = empty list - no errors, renders nothing (or
:elseblock if present) - 2-param syntax (
key, valoritem, idx) works for both arrays and objects:- Arrays: first = value, second = index
- Objects: first = key, second = value
- Brackets optional -
{#for key, val in obj}same as{#for [key, val] in obj}
Note on passing props: Use :prop="expr" syntax to pass JavaScript objects, arrays, or functions as props. Regular prop={expr} will stringify the value.
Component Isolation: Child components in loops are automatically preserved during parent re-renders. They only re-render when their props actually change - making loops with many items very efficient.
Preserving Elements with fez:keep
Use fez:keep to preserve plain HTML elements across parent re-renders. The element is only recreated when its fez:keep value changes.
Important: fez:keep must only be used on plain HTML elements (div, span, input, etc.), never on fez components. To preserve a fez component, wrap it in a plain HTML element with fez:keep:
<!-- Wrap child components in a plain element with fez:keep -->
{#each state.users as user}
<span fez:keep="user-{user.id}">
<user-card :user="user" />
</span>
{/each}
<!-- Wrap components in loops -->
{#for i in [0,1,2,3,4]}
<span fez:keep="star-{i}-{state.rating}-{state.color}">
<ui-star fill="{getStarFill(i)}" color="{state.color}" />
</span>
{/for}
<!-- Preserve form inputs to keep user-entered values -->
<input fez:keep="search-input" type="text" />
<!-- Preserve animation state -->
<div fez:keep="animated-element" class="slide-in">...</div>
How it works:
- Same
fez:keepvalue → Element is fully preserved (no re-render, all state intact) - Different
fez:keepvalue → Element is recreated from scratch - No
fez:keep→ Element may be recreated on every parent re-render
When to use:
- Wrapping child components in loops that have internal state
- Form inputs where you want to preserve user-entered values
- Elements with CSS animations you don't want to restart
- Any element where preserving DOM state is important
Best practice: Include all relevant state variables in the fez:keep value. This way the element is recreated exactly when it needs to be:
<!-- Good: wrapper recreates when fill changes, so star is recreated too -->
<span fez:keep="star-{i}-{getStarFill(i)}">
<ui-star fill="{getStarFill(i)}" />
</span>
<!-- Bad: wrapper never recreates even when fill changes -->
<span fez:keep="star-{i}">
<ui-star fill="{getStarFill(i)}" />
</span>
Async/Await Blocks
Handle promises directly in templates with automatic loading/error states:
<!-- Full syntax with all three states -->
{#await state.userData}
<p>Loading user...</p>
{:then user}
<div class="profile">
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
{:catch error}
<p class="error">Failed to load: {error.message}</p>
{/await}
<!-- Skip pending state (shows nothing while loading) -->
{#await state.data}{:then result}
<p>Result: {result}</p>
{/await}
<!-- With error handling but no pending state -->
{#await state.data}{:then result}
<p>{result}</p>
{:catch err}
<p>Error: {err.message}</p>
{/await}
class {
init() {
// CORRECT - assign promise directly, template handles loading/resolved/rejected states
this.state.userData = fetch('/api/user').then(r => r.json())
// WRONG - using await loses the loading state (value is already resolved)
// this.state.userData = await fetch('/api/user').then(r
