features

Every part of Fez,
shown one by one.

This is the complete working surface: component files, templates, state, props, directives, CSS, motion, lifecycle hooks, instance helpers, static APIs, the DOM differ, page navigation and CLI tools. Every item pairs the rule with a small example and the result it produces.

01 / 10

Components and loading

A component is a browser-native custom element described by one readable file. Load it directly, inline it, or register it from JavaScript.

.fez · no build step

Single-file components

Module imports, class logic, scoped and global styles, documentation, demo markup and the template can live together. The browser compiler handles the file directly.

<info>A friendly card.</info>
<demo><hello-card name="Ada" /></demo>

<script>
  class {}
</script>

<style>
  padding: 12px;
  b { color: #c8102e; }
</style>

Hello, <b>{props.name}</b>.
Hello, Ada.
compiled in the browser - no JSX, bundler or generated source folder

script fez · template fez · Fez.head · JSON template

Load components and their input

Use a script fez tag for a file, an inline xmp or template definition, or Fez.head() when the path is discovered at runtime. A preceding JSON template can supply large server-rendered props without putting them in an attribute.

<script fez="./fez/ui-card.fez"></script>

<xmp fez="inline-badge">
  <style>padding: 4px 8px;</style>
  <slot />
</xmp>

Fez.head({ fez: './fez/ui-dialog.fez' })

<script type="application/json">
  {"name":"Ada","roles":["admin"]}
</script>
<user-card data-json-template="true" />
ui-card readyinline-badge readyui-dialog readyJSON props parsed
definitions join one registry; JSON input passes through the same PROPS schema

Custom Elements · NAME

Native tags, semantic roots

Use the component as a real custom element. NAME chooses the rendered root, so a navigation component can become a genuine nav instead of a permanent wrapper.

<script>
  class {
    NAME = 'nav'
  }
</script>

<a href="/">Home</a>

<site-nav></site-nav>
<nav class="fez site-nav-fez">...</nav>
custom-element ergonomics with ordinary, stylable output

imports · head assets · import maps

A module zone before the class

Imports, shared constants and Fez.head() calls live before the component class. Import maps keep bare specifiers consistent across components; scripts and styles are deduplicated in head. A <head> block is appended to document.head as HTML, with its scripts run, in the browser and through the bundler plugin alike.

<script>
  Fez.head({ importmap: {
    three: 'https://esm.sh/three@0.180.0'
  }})
  Fez.head({ css: './map.css' })

  import * as THREE from 'three'
  const sceneColor = '#17181c'

  class { onMount() { /* use THREE */ } }
</script>
one shared module URL and one injected stylesheet
the class stays last; module-level work runs once per definition

META · info · demo · Fez.index

Documentation travels with the component

META, <info>, <demo> and raw source are indexed beside the class. Demo browsers and tooling can discover everything without a parallel manifest.

<info>Compact user avatar.</info>
<demo><ui-avatar name="Ada" /></demo>

<script>
  class { META = { category: 'identity' } }
</script>

Fez.index.get('ui-avatar')
Fez.index.withDemo()
classmetainfodemosource
one unified entry powers docs, demos and inspection

MOUNT = true · GLOBAL = 'Name'

Singleton components

Global overlays, toast hosts and dialogs append themselves to body exactly once with MOUNT. GLOBAL exposes the live instance on window for calls from ordinary page code, and works just as well for a tag you placed yourself.

<script>
  class {
    MOUNT = true
    GLOBAL = 'Dialog'

    open(message) {
      this.state.message = message
    }
  }
</script>

Dialog.open('Saved')
Saved  - one dialog instance in body
GLOBAL alone names a component you placed yourself, e.g. a viewport

components.txt · built-in components

Load a whole component library

A text file can list relative or absolute component paths. Fez also ships fez-component, fez-include, fez-inline and fez-demo for dynamic components, remote HTML, inline reactivity and catalogue pages.

# one component per line
ui-button
ui-dialog
forms/input-date
/shared/app-shell

Fez.head({ fez: './components.txt' })

<fez-component name="ui-button" />
<fez-include src="./partial.html" />
<fez-demo src="./components.txt" />
4 pathsresolved, fetched, compiled and indexed
extensions are optional; nested paths resolve from the list file

02 / 10

State and props

Everything the instance owns goes in state, shared values in globalState, and input from the parent in props. A state write re-renders only when the last render read that key.

this.state · fezReactiveStore · deep reactivity · batching

Assign state, get a render

Object and array mutations are observed deeply. Multiple writes in the same tick collapse into one animation-frame render, so ordinary JavaScript assignment is the full state API.

init() {
  this.state.user = { name: 'Ada' }
  this.state.tags = ['fast']
}

update() {
  this.state.user.name = 'Grace'
  this.state.tags.push('small')
  this.state.ready = true
}

makeDraft() {
  const draft = this.fezReactiveStore(
    { query: '' },
    (object, key, value) => audit(key, value),
    { shallow: true },
  )
  draft.query = 'Fez'
}
one renderGrace · fast, small · ready
three writes, one batched DOM morph

this.state · read tracking

Unrendered keys are free

Editors, charts, cached measurements, timers and normalized callbacks live in state too. Each render records the keys it read, so a write to a key the template never reads schedules nothing.

onMount() {
  this.state.chart = new Chart(this.state.canvas)
  this.state.last_width = this.root.offsetWidth
}

resize() {
  this.state.last_width = this.root.offsetWidth
}

<canvas fez:this="canvas"></canvas>
0 rendersthe template reads neither chart nor last_width
one store, and the render itself decides what is reactive

get name() · derived values

Class getters as derived values

A getter on the component class reads like a field in the template and is recomputed on every render, so a derived value never goes stale and never needs its own state key.

class {
  get total() {
    return this.state.items.reduce(
      (sum, item) => sum + item.price, 0
    )
  }
}

<p>Total: {total}</p>
Total: 48
reads inside the getter count as reads of the render

this.props · parent updates

Props remain live

Templates read props.name; methods read this.props.name. Top-level prop assignments render and flow to children, while nested object identity is preserved.

<h3>{props.user.name}</h3>
<child-card :user="props.user" />

rename(name) {
  this.props.user = {
    ...this.props.user,
    name
  }
}
Gracesame object passed to the child
do not copy changing props into state

PROPS · runtime coercion

Typed HTML attributes

The schema accepts String, Number, Boolean, Array, Object, Function, Date and custom casters. Values arrive typed before init().

PROPS = {
  title: String,
  page: Number,
  open: Boolean,
  items: Array,
  user: Object,
  on_pick: Function,
  since: Date,
  slug: raw => String(raw).toLowerCase(),
}

<ui-pager page="3" open items='["a","b"]' />
page: 3open: trueitems: Array(2)
no manual parseInt, JSON.parse or boolean string checks

default · required · enum · transform · state

Validation and state seeding

Defaults recover missing or invalid values, required reports omissions, enum limits choices, a one-argument default transforms raw input, and state seeds component-owned reactive data once.

PROPS = {
  size: {
    type: String,
    default: 'md',
    enum: ['sm', 'md', 'lg'],
  },
  tags: {
    type: Array,
    state: true,
    default: raw => (raw || '').split(','),
  },
  user: { type: Object, required: true },
}
size = mdstate.tags = [...]errors route through Fez.onError
state seeding copies arrays and plain objects shallowly

:prop · data-props · Fez.getFunction

Pass real values and callbacks

A colon attribute parks objects, arrays, booleans and functions without stringifying them. :x="expr", :x='expr' and :x={expr} are the same, names may be hyphenated, and object literals work. The expression runs in template scope, loop variables included. data-props accepts a JSON object, and handler props normalize through Fez.getFunction().

<user-card
  :user="state.current_user"
  :active="true"
  :on_pick={pick}
  :cfg="{dense: true}"
  :data-row="state.row"
/>

<user-card data-props='{"name":"Ada"}' />

PROPS = {
  on_pick: { type: Function, state: true, default: () => {} },
}
Object identity keptFunction callable
plain attributes stay strings; colon attributes stay JavaScript values

this.globalState · Fez.state · fez-inline

Shared reactive state

Reading a global key subscribes only that component. Writes notify onGlobalStateChange immediately and batch the required renders; only a key the last render read re-renders, so a read in a click handler fires the hook without painting. Static HTML can join through fez-inline.

increment() {
  this.globalState.count =
    (this.globalState.count || 0) + 1
}

Fez.state.get('count')
Fez.state.set('count', 4)
Fez.state.subscribe('count', (value, old) => {})
Fez.state.subscribe((value, old, key) => {})
Fez.state.delete('count')
Fez.state.forEach('count', component => {})
Fez.state.createProxy(component)

<fez-inline>Count: {globalState.count}</fez-inline>
Count: 4only readers of count refresh
global state subscriptions are tracked and cleaned automatically

GLOBAL_STATE · onMount · auto-cleanup

State tied to component presence

Declare global values on a component and Fez publishes them after onMount(), before onRefresh(). Function values run once, synchronously, with the live instance as this. Removing the component deletes its keys and refreshes their readers.

Presence means mounted in the DOM, regardless of scrolling or CSS visibility. Use unique keys per active instance: deletion is unconditional, with no restoration or instance counting. Use method syntax when reading this; arrow functions keep their original context.

Try the board toggle demo.

class {
  GLOBAL_STATE = {
    boardView: true,
    activeBoardId() {
      return this.state.boardId
    },
  }

  init(props) {
    this.state.boardId = props.board_id
  }
}

Fez.state.get('boardView') // true while mounted
// After removal: undefined
Mounted: boardView = trueRemoved: boardView = undefinedcomponents reading globalState.boardView update automatically
function values run once per mount; refresh does not recompute them

03 / 10

Template language

Fez templates stay close to HTML: braces for values, explicit blocks for control flow, and ordinary JavaScript expressions where a value belongs.

{expression} · attributes

Escaped expressions

Read state, props, globalState, component methods and getters in text or attributes. Every value is HTML-escaped as text, arrays and objects included; null and undefined render nothing. A backtick in text is a plain character, and script / style bodies inside a template are copied verbatim. Svelte-only syntax such as {@debug} or on:click fails with a hint at the Fez spelling.

<h2>{props.title}</h2>
<p>{state.user.name.toUpperCase()}</p>
<input value={state.query} />
<div data-count={state.items.length}></div>
TEAMADA · data-count="3"
write attr={value}; the compiler supplies safe quotes

{@html} · {@json}

Raw HTML and JSON output

{@html} deliberately inserts trusted markup without escaping. {@json} produces a readable JSON debug block for any value.

<div>{@html state.trusted_html}</div>

{@json state.user}
Formatted content
{"name":"Ada","active":true}
use raw HTML only with content you already trust

{@block name} · {@block:name}

Reusable template blocks

Define a small template fragment once and render it by name. Blocks share the template context, so they can read the same props, state and methods.

{@block avatar}
  <img src={props.avatar} alt={props.name} />
{/block}

<header>{@block:avatar}<h3>{props.name}</h3></header>
<footer>{@block:avatar}</footer>
avatar rendered twiceone block definition
named fragments avoid copying repeated template markup

{#if} · {:else if} · {:else}

Conditional branches

Use a readable block for alternate HTML trees. Closing and opening states produce genuinely different markup, which keeps the differ and render hash correct.

{#if state.status === 'loading'}
  <p>Loading...</p>
{:else if state.status === 'error'}
  <p class="error">Try again.</p>
{:else}
  <p>Ready.</p>
{/if}
Ready.
only the matching branch reaches the DOM

{#unless} · Fez truthiness

Empty-aware negative conditions

unless is the inverse of if. In Fez, empty arrays and empty objects are also falsy, alongside null, undefined, false, zero and an empty string.

{#unless state.results}
  <p>No results found.</p>
{/unless}

// all render the empty branch
state.results = []
state.results = {}
state.results = null
No results found.
non-empty arrays and objects are truthy

{#await} · {:then} · {:catch}

Promises in templates

Assign the Promise itself to state. The template shows pending markup immediately, then refreshes automatically for the resolved value or error.

init() {
  this.state.user = Fez.fetch('/api/user')
}

{#await state.user}
  <p>Loading user...</p>
{:then user}
  <p>Hello, {user.name}.</p>
{:catch error}
  <p>Failed: {error.message}</p>
{/await}

// not this: the template builds the promise
{#await load()}...{/await}
// a new promise every render: reported after 3 cycles
pendingthenorcatch
assign a fresh Promise to show the loading branch again; a rejection without {:catch} is reported

{#each} · implicit i · explicit index

Array loops

each exposes an implicit i, or an explicit index after the item name. A stable key preserves identity when items move.

<ol>
  {#each state.users as user, index}
    <li key={user.id}>
      {index + 1}. {user.name}
    </li>
  {/each}
</ol>

{#each state.tags as tag}
  <span>{i}: {tag}</span>
{/each}
1. Ada2. Grace3. Linus
keys let the differ move the existing node instead of replacing it

{#each items as item (item.id)}

Keyed each

Put the key in parentheses after the binding, Svelte style. It is written as key={...} on the first element of the loop body, so a reorder, insert or removal moves existing nodes and child components with their item.

Try the blocks demo.

{#each state.rows as row, index (row.id)}
  <user-row :user="row" />
{/each}

// compiles to
// <user-row key={row.id} :user="row" />
reverse()each child instance keeps its own state
an explicit key on the first element wins over the loop key

{#for} · arrays · objects · null safety

Collection and object loops

for accepts arrays and objects. Two parameters mean value/index for arrays and key/value for objects; brackets are optional. Null and undefined behave as empty collections.

{#for item in state.items}
  <span>{item}</span>
{/for}

{#for key, value in state.settings}
  <div>{key}: {value}</div>
{/for}

{#for [value, index] in state.items}
  <span>{index}: {value}</span>
{/for}
theme: darkdensity: compact
Fez.toPairs() supplies the same pair model to both loop forms

{:else} in loops

Empty-list fallbacks

An else branch belongs directly to each or for, so loading an empty collection does not need a separate conditional.

{#each state.notifications as note}
  <article key={note.id}>{note.text}</article>
{:else}
  <p class="empty">You are all caught up.</p>
{/each}
You are all caught up.
works for [], {}, null and undefined

{@const name = expr}

Local constants

Name a value once for the rest of its branch, destructuring included. {:else}, {:then}, {:catch} and the closing tag end it, and event handlers capture it at render time like a loop variable.

{#each state.items as item}
  {@const total = item.price * item.qty}
  <p>{item.name}: {total}</p>
  <button onclick={() => pick(total)}>Pick</button>
{/each}

{@const { first, last } = state.user}
<h3>{first} {last}</h3>
Alpha: 6Bravo: 10
no beforeRender bookkeeping for a value used in one row

{#key expr}

Recreate a subtree on change

The body of a key block is rebuilt instead of morphed whenever the value changes: new nodes play their fez:in again, and child components inside start over through init().

{#key state.slide}
  <img src={state.slide.url} fez:in="fade" />
{/key}
slide 1 -> 2new img, fade plays again
same value, same nodes: the body morphs as usual

name={expr} · boolean attributes · class:name

Dynamic attributes and classes

Use a normal expression or ternary inside attributes. An unquoted name={expr} writes the whole attribute: null and undefined drop it, false drops a boolean attribute such as disabled or hidden but renders "false" on others like aria-expanded, and true on a boolean attribute renders name="". The class:name directive merges conditional classes without overwriting the static class list.

<button
  class="btn"
  class:active={state.active}
  class:loading={state.loading}
  disabled={state.loading}
  aria-expanded={state.open}
  title={state.tip}
  aria-label={state.active ? 'Deactivate' : 'Activate'}
>Save</button>
class="btn active"aria-expanded="false"no title, not disabled
control-flow blocks are never placed inside an attribute; Fez.htmlAttr(name, value) is the runtime rule

style:prop={value} · style:--custom

One CSS property per directive

A style directive merges one property, custom properties included, into the style attribute after its own declarations. null, undefined and false leave the property out.

<div
  style="padding: 4px"
  style:color={state.color}
  style:width={state.w + 'px'}
  style:--accent={props.accent}
>...</div>
style="padding: 4px;color: red;width: 120px;"
accent unset, so --accent is left out; Fez.styleProp is the runtime helper

inline handlers · arrows · method refs · strict !

Three event-handler forms

String handlers are concise for scalar values. Unquoted arrow handlers capture complex loop objects; async arrows work and the first parameter is the event whatever its name. onclick={save} calls the method with the event. Appending ! fires only on the element itself and also prevents default and stops propagation.

<button onclick="fez.save()">Save</button>
<input onchange="fez.rename(this.value)" />
<button onclick={save}>Save</button>
<a onclick={(evt) => go(evt.target)}>Go</a>
<button onclick={async () => load()}>Load</button>

{#each state.tasks as task}
  <button onclick={() => edit(task)}>Edit</button>
{/each}

<div class="overlay" onclick!="fez.close()">
  <button>Does not close overlay</button>
</div>
scalar inlineobject capturedstrict target
arrow functions are compiled into stable Fez handler slots

self-closing custom tags

HTML correction

Self-closing custom elements are expanded to valid paired tags before the browser can mis-nest them.

<ui-icon name="star" />
<user-card :user="state.user" />
<ui-icon name="star"></ui-icon>
write the short form, the browser sees the paired tags

no whitespace between nodes · {' '}

Spacing comes from CSS

Whitespace-only gaps between tags and blocks are dropped, on one line or across lines. A gap would render as a font-dependent space, so space elements with gap or margin, or write {' '} where a real space belongs. Text keeps its own spaces, and pre / textarea stay as written.

<nav>
  <a href="/">Home</a>   <a href="/docs">Docs</a>
  {#if state.admin}
    <a href="/admin">Admin</a>
  {/if}
</nav>
<p>Hello <b>{props.name}</b>{' '}<i>again</i></p>
<nav><a href="/">Home</a><a href="/docs">Docs</a><a href="/admin">Admin</a></nav><p>Hello <b>Ada</b> <i>again</i></p>
every gap you see is one you wrote in CSS or with {' '}

indentation · Tailwind shorthand · no wrapper

Slim templates

Write the template in Slim instead of HTML - a template whose first line reads as Slim is detected, no wrapper needed. Tags nest by indentation, .class / #id shorthand takes Tailwind classes as written (p-0.5, w-1/2, bg-[#fff], hover:bg-gray-100), and - if / - each lines replace the block tags. Ruby Slim forms like - list.each do |item| work too.

It compiles to a normal Fez template in the browser, the CLI and the bundler plugin. Errors point at the .fez line, and fez compile -t prints the generated template beside the Slim lines. Plain text or <tag> first lines stay HTML; a <slim> block forces Slim.

.card.p-4.md:w-1/2.bg-[#fafafa]
  h3.text-lg.font-semibold= props.title
  - each state.items as item
    span.text-sm.px-1.5 = item.name
  - else
    p.text-gray-400 No items
  button.px-3.py-1.5 onclick="fez.add()" Add
<div class="card p-4 md:w-1/2 bg-[#fafafa]"><h3 class="text-lg font-semibold">{props.title}</h3>{#each state.items as item}<span class="text-sm px-1.5">{item.name}</span>{:else}<p class="text-gray-400">No items</p>{/each}<button class="px-3 py-1.5" onclick="fez.add()">Add</button></div>
the first line decides: it reads as Slim, so the template is Slim

04 / 10

Directives, slots and forms

Special fez: attributes connect template nodes to component behavior without hiding the resulting DOM.

fez:bind

Two-way form binding

Bind an input, select or textarea to state when the component should re-render as the value changes. The directive updates the target path and schedules the render.

init() {
  this.state.volume = 40
}

<input type="range" min="0" max="100"
       fez:bind="state.volume" />
<output>{state.volume}%</output>
40%
for large forms, prefer fez:this and read values only on submit

fez:this · stable auto-ID

Direct element references

fez:this puts the live node in this.state after every render, silently: the ref write fires no hook and schedules no render. Static reference names also receive a stable generated ID, one per row inside a loop, helping the differ preserve each typed value and focus.

<input fez:this="name_input" />

submit() {
  const name = this.state.name_input.value
}

// generated automatically
// id="fez-42-name_input"
this.state.name_inputpoints to the preserved input node
refs are ready in onMount(), not init()

fez:use · update · destroy

Run a DOM hook on insertion

A use directive calls a component method or inline function with the inserted node, once per node. The action may return a cleanup function, or { update(node), destroy() }: update runs on later renders, destroy when the node leaves or the component is destroyed.

<input fez:use="focusInput" />
<span fez:use="tooltip" title={state.tip}>?</span>

focusInput(node) {
  node.focus()
}

tooltip(node) {
  const tip = new Tooltip(node)
  return {
    update: (node) => tip.setText(node.title),
    destroy: () => tip.remove(),
  }
}
input focused when its node is added
the callback receives the actual live DOM element; a returned function is the cleanup

fez:class

Delayed classes

The value is split on spaces: every class but the last is added right after render, the last one a millisecond later. That gap lets the initial state paint, so a CSS transition runs into the final class.

<span fez:class="fade visible">Ready</span>

<style>
  span { opacity: 0; transition: opacity .2s; }
  span.visible { opacity: 1; }
</style>
+ fade now+ visible 1ms later
for full enter and leave animation, use fez:in and fez:out

fez:keep · key · child isolation

Preserve DOM identity

A stable key tells the differ to keep and move the same plain element. Child Fez components are isolated automatically; wrap one in a keyed plain element when the parent also controls its identity.

{#each state.users as user}
  <article key={user.id} fez:keep="user-{user.id}">
    <user-card :user="user" />
  </article>
{/each}

<input fez:keep="search-input" />
focus keptcomponent state keptnode moved
fez:keep belongs on plain HTML, never directly on a Fez component

slot · slot unwrap · fezSlot

Slots that preserve listeners

A normal slot keeps children in a .fez-slot wrapper. unwrap dissolves that wrapper into the parent. Fez moves the real nodes, so existing listeners survive. There is one default slot; named slots are not supported, so pass extra markup as a child component.

<section class="panel">
  <slot />
</section>

// or direct children, no wrapper
<nav><slot unwrap /></nav>

// low-level equivalent
this.fezSlot(source, target)
default wrapperordirect children
slot unwrap components render once and cannot use reactive state

onSubmit · formData()

Forms become plain objects

Define onSubmit(data) and every form the component renders submits into it with its named fields, including a form rendered later inside {#if}; a child component's form is left to the child. Call formData() directly when submission is controlled elsewhere.

onSubmit({ name, role }) {
  this.save({ name, role })
}

saveDraft() {
  const data = this.formData()
}

<form>
  <input name="name" value="Ada" />
  <select name="role">...</select>
</form>
{ name: 'Ada', role: 'admin' }
one delegated submit listener on the root, default submit prevented

05 / 10

CSS and motion

Scoping is explicit, nesting is built in, responsive and theme rules stay compact, and Web Animations power enter, leave, reorder and size changes.

style · wrapper & · nested selectors

Scoped, nested component CSS

Every rule in <style> is scoped to the generated component class. Root declarations style the wrapper; & addresses it explicitly; nested selectors flatten to ordinary CSS.

<style>
  padding: 18px;          /* component wrapper */

  &:hover { border-color: #c8102e; }

  article {
    display: grid;
    h3 { margin: 0; }
    a:hover { color: #c8102e; }
  }
</style>
.ui-card-fezcontains every emitted selector
no CSS Modules, Sass compiler or shadow DOM required

:global(...) · Svelte semantics

Escape the scope one selector at a time

Inside scoped CSS, :global(...) works the Svelte way: the part inside it is never scoped. Alone it escapes the component; followed by more selector it sits above the scope, and & places the scope explicitly.

<style>
  .toolbar { display: flex; }
  :global(.mapboxgl-popup) { z-index: 20; }
  :global(.dark) .btn { color: #fff; }
  :global(html.rtl) & .icon { transform: scaleX(-1); }
</style>

/* .mapboxgl-popup
   .dark .fez.x-fez .btn
   html.rtl .fez.x-fez .icon */
escapesabove the scopescope at &
a list inside :global(.a, .b) expands to one selector per item

style global

Document-wide tokens and classes

A <style global> block is emitted document-wide, verbatim and once, however many instances mount. Scope is declared on the style tag, never guessed from selectors. Use it for :root tokens, @font-face and app-wide classes.

<style global>
  :root { --brand: #c8102e; }
  .app-shell { min-height: 100vh; }
</style>
global channelinjected once
wrapping a global rule in :global(...) is a compile error - it already is global

@keyframes · @font-face · @property · @import · @layer

Legal at-rules from scoped files

Rules that cannot legally nest inside a component selector are hoisted automatically, @position-try and @view-transition included. Media, supports, container, layer blocks and @starting-style remain nested around the scoped selector. @import, @charset and @layer a, b; move to the front of the sheet even from a scoped block. Comments are /* */ only; a // line breaks the next rule.

<style>
  @import url("./tokens.css");
  .notice { animation: enter .25s ease-out; }

  @keyframes enter {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
  }

  @media (min-width: 800px) {
    .notice { padding: 20px; }
  }
</style>
@import first@keyframes hoisted@media stays scoped
@import and @layer statements land in a separate <style id="fez-css-head"> before the shared sheet

:mobile · :tablet · :desktop

Responsive style macros

Built-in macros expand wherever they are nested: mobile is up to 767px, tablet is 768-1023px, and desktop begins at 1200px. @include name is an equivalent spelling.

<style>
  .grid {
    grid-template-columns: repeat(3, 1fr);

    :tablet { grid-template-columns: repeat(2, 1fr); }
    :mobile { grid-template-columns: 1fr; }
  }

  :desktop { max-width: 1200px; }
  @include mobile { padding: 12px; }
</style>
mobile: 1tablet: 2desktop: 3
the space before { is required for macro expansion

:dark · zero specificity

Dark mode without a theme runtime

The dark macro matches any component under html.dark using zero-specificity :where(). Toggle the class yourself, or redefine the macro to follow the operating system.

<style>
  .card {
    color: #17181c;
    background: #fff;
    :dark { color: #eee; background: #17181c; }
  }
</style>

document.documentElement.classList.toggle('dark', true)

// optional OS-driven behavior
Fez.cssMixin('dark',
  '@media (prefers-color-scheme: dark)')
Dark card
put the class on html so root tokens and overscroll match too

cssMixin · globalCss · cssClass · extractCss · color helpers

Programmable CSS utilities

Register selector, at-rule or declaration macros, inject deduplicated global nested CSS, create a generated utility class, and extract every injected rule in order for debugging or server rendering.

Fez.cssMixin('motion-ok',
  '@media (prefers-reduced-motion: no-preference)')

// `;` instead of `{` inlines the body as declarations
Fez.cssMixin('card', 'padding: 16px; border-radius: 8px')
// .item { :card; color: red; }

Fez.globalCss('.toast { &.error { color: red } }')

const klass = Fez.cssClass('display: grid; gap: 12px;')
const allCss = Fez.extractCss()

Fez.darkenColor('#c8102e', 15)
Fez.lightenColor('#c8102e', 15)
:motion-okdeduped sheetgenerated classcolor shades
the same flattener handles component and runtime CSS

fez:in · fez:out · fez:transition

Enter and leave transitions

Intro runs when a plain element appears. Outro delays removal until animation completes. fez:transition applies one spec both ways, while explicit in or out attributes can override either direction.

{#if state.open}
  <div
    fez:in="fly, from=bottom, duration=280"
    fez:out="fade, duration=140"
  >Saved</div>
{/if}

<div fez:transition="pop, duration=220">Menu</div>
insertfly inremovefade, then detach
duration, delay and easing work on every transition

fade · fly · slide · scale · pop · blur · flip · rotate · draw

Nine built-in transitions

Cover opacity, directional movement, accordion layout, scaling, overshoot, blur, 3D flips, rotation and SVG stroke drawing. Named Svelte-style easings and CSS timing functions are both accepted.

<div fez:in="fade">...</div>
<div fez:in="fly, from=left, distance=30">...</div>
<div fez:in="slide, axis=x">...</div>
<div fez:in="scale, start=.7">...</div>
<div fez:in="pop, easing=backOut">...</div>
<div fez:in="blur, amount=8">...</div>
<div fez:in="flip, axis=y">...</div>
<div fez:in="rotate, angle=-120">...</div>
<path fez:in="draw, speed=2" />
9 effectsnamed easingreduced motion safe
non-SVG draw targets fall back to fade

Fez.transitions · CSS @keyframes names

Custom transition functions

Add a transition factory to the registry and return keyframes plus timing. An unknown transition name falls back to a CSS @keyframes animation of that name.

Fez.transitions.wobble = (node, params) => ({
  keyframes: [
    { transform: 'rotate(-4deg)', opacity: 0 },
    { transform: 'none', opacity: 1 },
  ],
  duration: params.duration || 240,
  easing: 'backOut',
})

<div fez:in="wobble, duration=300">Hello</div>
<div fez:in="my-css-keyframes">Hello</div>
registry functionorCSS animation name
first keyframe is hidden; last frame is the natural state

fez:animate="flip"

Animate list reordering

FLIP measures keyed nodes before and after the morph, then animates the position delta. Insertions can use fez:in; removals can use fez:out.

{#each state.items as item}
  <li
    key={item.id}
    fez:animate="flip, duration=250"
    fez:in="fly, from=left"
    fez:out="fade, duration=140"
  >{item.name}</li>
{/each}
insert fliesreorder glidesremove fades
identity is required so the differ can recognize a moved node

height · width · size · Fez.animateSize

Animate content size

A ResizeObserver animates a kept element from its old box to its new one, including changes caused by nested components or outside DOM code. Combine size and FLIP specs when both change.

<div fez:animate="height, duration=250">
  {#if state.open}
    <p>Expandable content</p>
  {/if}
</div>

<li fez:animate="flip, height">...</li>

Fez.animateSize(node, 'width, duration=180')
heightwidthboth axes
inline dimensions and overflow are restored after animation

06 / 10

Lifecycle hooks

Every lifecycle moment has a named method. Use only the hooks the component needs; Fez supplies empty implementations for the rest.

init(props)

Initialize before rendering

init runs once after props are coerced and before the first template render. Initialize every reactive state key here; original slot children are still present, but template refs are not.

init(props) {
  this.state.count = props.start || 0
  this.state.open = false
  this.state.on_save = Fez.getFunction(props.on_save)
}
props typedstate readytemplate has not rendered yet
PROPS state seeding happens just before init

beforeRender()

Compute derived state

This hook runs immediately before every render, including the first. Derive values from current state and props here so templates never see stale calculations.

beforeRender() {
  this.state.full_name =
    `${this.state.first} ${this.state.last}`
  this.state.total = this.state.items.reduce(
    (sum, item) => sum + item.price, 0
  )
}
Ada LovelaceTotal: $48
the Fez replacement for reactive declarations and computed stores

onMount(props)

Work with the rendered DOM

onMount runs once after the first DOM render. Template refs, layout measurements, focus and third-party libraries belong here. A returned function is the cleanup, run on destroy.

onMount(props) {
  this.state.name_input.focus()
  const editor = new Editor({
    element: this.state.editor_root,
  })
  return () => editor.destroy()
}

<input fez:this="name_input" />
<div fez:this="editor_root"></div>
refs point to connected, rendered elements
a throw in init, the first render or onMount is reported and the instance destroyed

afterRender()

React after every morph

This hook runs after the rendered HTML has been applied, and also when the render hash matched and the DOM was left alone. Use it for a small side effect that genuinely needs the latest DOM after every component render.

afterRender() {
  this.root.style.setProperty(
    '--rows',
    this.root.querySelectorAll('li').length
  )
}
DOM morphed--rows: 6 applied afterward
avoid state writes here unless a second render is intentional

onRefresh(props, changed)

Know when a parent preserves you

onRefresh runs after mount and whenever this instance is kept and handed props: by a parent render, a pjax or Fez.nodeMorph swap, or fez-component. changed holds the props whose value differs (every prop at mount). The child already re-rendered for a changed prop. It does not fire for the child's own state render or for outside DOM events.

onRefresh(props, changed) {
  if ('revision' in changed) this.load()
}
parent morphedchild instance stayed connected
use this.on for document, window and Pjax events

onStateChange(key, value, oldValue)

Observe local state writes

Runs when reactive local state changes, with the top-level key and old and new values. The pending UI render remains batched into the next frame.

onStateChange(key, value, oldValue) {
  if (key === 'count') {
    this.globalState.max = Math.max(
      value,
      this.globalState.max || 0
    )
  }
}
count: 2 → 3global max becomes 3
nested writes report through their owning top-level state key

onGlobalStateChange(key, value, oldValue)

Observe shared state

Components that read a global key receive this hook synchronously when that key changes. A key the last render read also schedules one render alongside any local state changes; a key read only in code, as below, fires the hook alone.

init() {
  this.globalState.theme // subscribe by reading
}

onGlobalStateChange(key, value, oldValue) {
  if (key === 'theme') {
    document.documentElement.classList.toggle(
      'dark', value === 'dark'
    )
  }
}
theme: light → darksubscribers update together
a component's own write during render does not schedule an extra pass

addOnDestroy() · onDestroy()

Release external resources

When MutationObserver sees the component disconnect, Fez runs callbacks registered with addOnDestroy(), removes listeners, observers, timers and subscriptions, then calls onDestroy for external resources.

onDestroy() {
  this.state.editor?.destroy()
  this.state.chart?.dispose()
}

onMount() {
  const observer = new MutationObserver(this.track)
  observer.observe(this.root, { childList: true })
  this.addOnDestroy(() => observer.disconnect())
}

// already cleaned by Fez:
// this.on(...)
// this.setTimeout(...)
// this.setInterval(...)
// this.subscribe(...)
// this.observeResize(...)
// onMount and fez:use cleanups
listeners removedtimers clearedsubscriptions dropped
cleanup runs once, even if the node is removed more than once; a throwing onDestroy is reported and teardown completes

onError(error, component)

Error boundaries

Any ancestor that defines onError catches a descendant's mount error, render throw or template runtime error, with the failing instance as component. Set state to show a fallback, or throw to pass the error further up. Without a boundary, Fez.onError reports it as before; a failed template always keeps its last rendered DOM.

Try the context and boundary demo.

onError(error, component) {
  this.state.error =
    `<${component.fezName}> ${error.message}`
}

<risky-widget />
{#if state.error}
  <p class="error">{state.error}</p>
{/if}
<risky-widget> failed to start
a child that fails to mount is destroyed; one that fails to render keeps its last DOM

this.context(key, value?)

Context down the component tree

this.context(key, value) provides a value; this.context(key) reads it from the nearest component up the tree, the caller included. Read it in init(). Context is not reactive: provide an object, or use globalState for values that change.

// provider
init() {
  this.context('theme', { color: '#1f7a8c' })
}

// any descendant, however deep
init() {
  this.state.theme = this.context('theme')
}
theme found 3 levels up
Svelte's setContext and getContext in one call

07 / 10

Core instance methods

Every component keeps direct access to its live root, focused DOM shortcuts, rendering controls, safe events, timers and component-level communication.

root · find · val

Read and write the live DOM

root is the mounted component element. find() scopes a selector to it. val() gets or sets form values, checked state, or innerHTML for non-form nodes.

const button = this.find('button.save')
const query = this.val('input[name=search]')

this.val('input[name=search]', 'Fez')
this.val('.preview', '<b>Ready</b>')

this.root.append(document.createElement('hr'))
scoped queryinput valuelive DOM write
Fez does not hide or replace browser DOM APIs

this.n()

Build native nodes in JavaScript

The built-in node builder accepts a tag, dot-separated classes, attributes and text or child nodes. Functions become bound DOM properties, boolean attributes follow HTML rules, and arrays append every child.

const save = this.n('button.primary', {
  type: 'button',
  disabled: false,
  onclick: this.save,
}, 'Save')

const row = this.n('.actions', [
  save,
  this.n('span.hint', 'Ready'),
])

this.root.append(row)
Ready
this.n('.actions', ...) defaults to a div with class="actions"

attr · copy · setAttribute

Attributes and DOM properties

attr() gets or sets a root attribute. copy() forwards chosen input props to the semantic root. From outside, Fez(el).setAttribute(name, value) feeds a value through PROPS into this.props.

init() {
  this.copy('href', 'target', 'aria-label')
  this.attr('data-ready', 'true')
  this.props.href // read props directly
}

Fez('#docs-link').setAttribute('target', '_self')
<a href="/docs" target="_blank" data-ready="true">
non-string values copied by copy() land as DOM properties

setStyle · classList

Focused presentation helpers

Set one or many CSS custom properties and styles on the root with setStyle(). Classes go through the native classList, or a class:name directive in the template.

this.root.classList.add('ready', 'compact')
this.root.classList.toggle('open', this.props.open)

this.setStyle('--accent', '#c8102e')
this.setStyle({ color: 'white', background: '#17181c' })
ready compact · --accent set
setStyle targets the component root

rootId · UID · fezName · fezHtmlRoot · isConnected

Stable identity and connection state

rootId() returns the existing root ID or assigns a unique one. isConnected reports whether the component root is currently attached to the document.

onMount() {
  const id = this.rootId()
  console.log(id)             // fez_42
  console.log(this.UID)       // 42
  console.log(this.fezName)   // user-card
  console.log(this.fezHtmlRoot) // Fez(42).
  console.log(this.isConnected) // true
}
fez_42connected
late async callbacks can check connection before touching the page

childNodes · childObjects · dissolve

Work with original children

Read child elements as nodes, map them, or turn them into objects containing HTML, root references, node names and attributes. dissolve() removes the component wrapper while keeping its children.

const nodes = this.childNodes()
const labels = this.childNodes(node => node.textContent)
const objects = this.childObjects()
// [{ html, ROOT, NODE_NAME, title, ...attrs }]

const moved = this.dissolve()

// or replace the wrapper with a semantic node
this.dissolve(document.createElement('nav'))
nodesmapped valuesobjectsunwrapped
child node references remain the real nodes throughout

refresh · tick · fezRender · fezParseHtml · fezNextTick

Rendering controls

refresh() schedules the normal render. await this.tick() resolves once the pending render has reached the DOM. fezRender() renders now, the configured template or a supplied one. fezNextTick() deduplicates named work into the next animation frame, or a timer while the tab is hidden.

this.refresh()

async open() {
  this.state.open = true
  await this.tick()
  this.find('input').focus()
}

this.fezRender()
this.fezRender('<p>Temporary markup</p>')

const parsed = this.fezParseHtml(templateText)

this.fezNextTick(() => {
  this.measureLayout()
}, 'measure-layout')
one next-frame rendernamed work deduped
normal state and prop assignments already schedule refresh automatically

on · observeResize · Fez.WINDOW_EVENTS

Auto-cleaned event listeners

on() binds this, guards disconnected instances, supports throttling and returns a disposer; window resize and scroll go through it too, and the handler is not called at bind time. observeResize() uses ResizeObserver and calls once right away with the element's rect.

const off = this.on('pjax:render', this.followPage)
this.on(window, 'keydown', this.onKey)
this.on(this.root, 'pointermove', this.move, { throttle: 50 })

this.on('resize', this.layout, { throttle: 100 })
this.on('scroll', this.markSection, { throttle: 80 })
this.layout() // first run, if needed

this.observeResize(this.state.panel, (rect, el) => this.fit(rect), 120)

Fez.WINDOW_EVENTS.add('app:layout')

off() // optional early removal
windowdocumentany EventTargetResizeObserver
known window events default to window; all other string events default to document

setTimeout · setInterval

Timers that cannot outlive a component

Both helpers skip callbacks after disconnect and clear themselves during destruction. Named intervals replace an earlier interval with the same purpose.

this.setTimeout(() => {
  this.state.saved = false
}, 1200)

this.setInterval(() => {
  this.state.now = new Date()
}, 1000, 'clock')

// a second interval named "clock" replaces the first
timeout trackedinterval trackedboth cleared on destroy
use these instead of manual window timer cleanup

publish · subscribe

Component-level messaging

A component publish walks up through parent components until a matching subscriber handles it. Subscriptions clean up automatically on destroy, survive a move of the component, and return an unsubscribe function.

// child
select(user) {
  this.publish('user-selected', user)
}

// parent
init() {
  this.subscribe('user-selected', user => {
    this.state.selected = user
  })
}
child publishes→nearest parent handles
use Fez.publish for a document-wide broadcast instead

08 / 10

Runtime APIs

The global Fez object exposes registration, lookup, component metadata, fetch and storage, global messaging, DOM utilities, compilation and diagnostics.

Fez(name, class) · Fez(selector) · Fez.find

Register and find components

The main function registers a class or finds instances by UID, selector, DOM node, component name and parent context. A callback visits every live instance of a name and the call returns those instances. Fez.find returns undefined when nothing matches.

Fez('status-pill', class {
  NAME = 'span'
})

Fez('#main-dialog').open()
Fez(42)
Fez(node)
Fez('ui-tabs', this.root)
const all = Fez('ui-tabs', tabs => tabs.refresh())
Fez.find(node, 'ui-tabs')
Fez.version // from package.json
registerone instanceall instancesparent context
Fez.instances and Fez.instanceCount expose the live registry directly

Fez.index

Inspect the component catalogue

Read class, schema, metadata, demo, info and source; render a stored demo; list every component or only documented ones; retrieve all records; ensure an entry; or print the index.

Fez.index['ui-card'].class
Fez.index['ui-card'].props
Fez.index['ui-card'].meta
Fez.index['ui-card'].demo
Fez.index['ui-card'].info
Fez.index['ui-card'].source

Fez.index.get('ui-card')
Fez.index.apply('ui-card', target)
Fez.index.names()
Fez.index.withDemo()
Fez.index.all()
Fez.index.ensure('ui-card')
Fez.index.info()

const Card = Fez.index['ui-card'].class
Card.propsSchema()
Card.castProp('open', 'false', 'ui-card')
Card.castProps({ count: '3' }, 'ui-card')
Card.toBoolean('off')
source of truthfor docs, demos, tooling and inspection
the PROPS schema is available to generators without constructing an instance

Fez.fetch · clearFetchCache

Fetch with parsing and cache

Promise and callback forms support GET query params and POST FormData. JSON parses automatically, GET responses cache in memory, and simultaneous identical requests share one in-flight Promise.

const users = await Fez.fetch('/api/users')

Fez.fetch('/api/search', { q: 'fez' }, data => {})

await Fez.fetch('POST', '/api/users', {
  name: 'Ada',
})

Fez.clearFetchCache()
JSON parsedGET cachedin-flight dedupederrors → onError
response content type decides whether the value is JSON or text

Fez.localStorage

Typed local storage

Values serialize and parse as JSON, so numbers, booleans, arrays and objects retain their types. Missing keys can return a caller-supplied fallback.

Fez.localStorage.set('count', 42)
Fez.localStorage.get('count') // 42, a Number

Fez.localStorage.set('user', { name: 'Ada' })
Fez.localStorage.get('user')

Fez.localStorage.get('missing', 'fallback')
Fez.localStorage.remove('count')
Fez.localStorage.clear()
42 Number{ name: 'Ada' }fallback
the helper uses browser localStorage underneath

Fez.publish · Fez.subscribe

Global pub/sub with targeting

Broadcast to every subscriber, or attach a subscriber to a node or selector so it fires only while that target is connected. Every subscribe call returns an unsubscribe function.

const off = Fez.subscribe('signed-in', user => {})

Fez.subscribe(node, 'refresh', callback)
Fez.subscribe('#modal', 'refresh', callback)

Fez.publish('signed-in', { id: 7 })
Fez.publish('refresh')

off()
broadcastconnected node onlylive selector only
component subscribe is hierarchical; Fez.subscribe is global

domRoot · tag · htmlEscape · htmlAttr · styleProp · activateNode · nodeMorph

Low-level DOM utilities

Create a root from HTML, build encoded tags, escape any value as text, render one attribute or style declaration by the template rules, activate one sibling, or invoke the component-aware DOM differ directly. nodeMorph accepts an HTML string, an element or a fragment, plus advanced matching options.

const root = Fez.domRoot('<section>Hello</section>')
const html = Fez.tag('button', { disabled: true }, 'Save')
const safe = Fez.htmlEscape('<b>unsafe</b>')
Fez.htmlAttr('hidden', false)        // ''
Fez.styleProp('color', 'red')        // 'color: red;'

Fez.activateNode(tab, 'active')
Fez.nodeMorph(target, '<p>new HTML</p>')
Fez.nodeMorph(target, fragment, {
  skipNode: node => node.matches('.external')
})
HTML → nodeprops encodedsibling activatedDOM morphed
nodeMorph is the lower-level engine used by components and Pjax

getFunction · isTrue · isTruthy · typeof · toPairs

Value normalization helpers

Resolve handler props, parse HTML-like booleans, apply Fez's empty-collection truthiness, identify short value types, and normalize arrays or objects to loop pairs.

Fez.getFunction(props.onclick)

Fez.isTrue('false')       // false
Fez.isTruthy([])          // false
Fez.typeof({})            // 'o'
Fez.typeof(1.5)           // 'n'
Fez.typeof(true)          // 'b'

Fez.toPairs(['a', 'b'])   // [['a',0], ['b',1]]
Fez.toPairs({ a: 1 })     // [['a',1]]
Fez.toPairs(null)         // []
functionbooleantype codepairs
getFunction returns a safe empty function for missing handlers

throttle · untilTrue · onReady · fnv1 · uid · pointer

Control-flow and extension helpers

Throttle noisy functions, poll until a condition succeeds, wait for DOM readiness, hash text, generate IDs, park callbacks for HTML, and extend the global Fez namespace with project utilities.

const onMove = Fez.throttle(move, 50)
Fez.untilTrue(() => document.querySelector('#ready'), 100)
Fez.onReady(boot)

Fez.fnv1('same input')
Fez.uid()
Fez.pointer(callback, { persist: true })

Fez.sweepPointers()
rate limitedDOM readystable hashsafe callback pointer
pointer entries are swept automatically unless marked persistent

compile · createTemplate · LOG · DEV · onError · dump · highlightAll

Compilation and diagnostics

Compile source or create a template function at runtime. Enable logs and component overlays, inspect a component dump, customize errors, or use the low-level console helpers.

Fez.compile('status-pill', source)
const render = Fez.createTemplate('<b>{state.name}</b>', {
  name: 'status-pill',
})

Fez.LOG = true
Fez.DEV = true
Fez.onError = (kind, error, context) => report(error)
Fez.consoleLog('rendered')
Fez.consoleError('invalid prop')
this.fezError('editor', 'Could not mount', { node: this.root })
Fez.dump(this.state)
Fez.highlightAll()
compiledloggedinspectedhighlighted
development overlays can be toggled with Cmd/Ctrl + E

09 / 10

DOM and performance

Fez renders HTML, then carefully morphs the live DOM. Components and important nodes keep their identity, and large specialized widgets can opt into direct DOM speed.

real DOM morph · component boundaries

A component-aware differ

There is no virtual DOM. The rendered string becomes real nodes and morphs the existing tree. Child Fez components are moved and receive prop refreshes, but their internal DOM and state are never morphed by the parent.

// parent changes its own state
this.state.heading = 'Updated'

<h2>{state.heading}</h2>
<user-card :user="state.user" />

// result:
// h2 text is morphed
// user-card instance is preserved
// only changed props reach the child
parent DOM patchedchild state keptfocus kept
server HTML and external DOM remain first-class citizens

fez:keep · fez-key · key · id · source signature

Priority-based node matching

The differ prefers explicit identity, then stable IDs and source signatures, with a scored soft-match fallback. Reordering can therefore move existing nodes instead of destroying them.

1. fez:keep="invoice-7"
2. fez-key="invoice-7"
3. key="invoice-7"
4. id="invoice-7"
5. matching element source/signature
6. scored soft match

<li key={invoice.id}>{invoice.total}</li>
same nodemoves from row 4 to row 1
strong identity also enables reliable FLIP animation

FNV-1 hash · render slots · requestAnimationFrame

Two levels of render skipping

State and prop writes batch into one frame. If the rendered HTML hash is unchanged, the morph is skipped; a changed evaluated object or handler slot still forces the necessary child prop update.

this.state.a = 1
this.state.b = 2
this.state.c = 3
// one animation-frame render

// same HTML + same render slots
// → skip morph

// same HTML + changed :user object
// → morph so child props update
3 writes → 1 rendersame output → 0 morphs
hash skipping is automatic and requires no memo API

active input · classList sync · scroll · animation

Preserve what the user is doing

Focused input values and checked state are protected from render sync. Class changes use classList rather than replacing the attribute, so running CSS animations survive. Stable nodes keep scroll and selection state.

// while the user types into the focused input:
this.state.server_status = 'saved'

// Fez updates the status elsewhere but preserves:
// - active input value and checked state
// - focus and selection
// - element scroll position
// - running CSS animation classes
typing continuesfocus staysanimation stays
the live DOM, not a recreated tree, remains the source of interaction state

fez:keep subtree · unrendered state · controller-only

Three direct-DOM escape hatches

For a frequently updated component with hundreds or thousands of nodes, protect one subtree, keep the data in state keys the template never reads, or omit the template entirely. Lifecycle, props, CSS, events and cleanup still work.

// 1. reactive shell, protected body
<h3>{state.title}</h3>
<div fez:keep="grid" fez:this="grid"></div>

// 2. unrendered state key: no render
this.state.rows = []

// 3. no template: pure controller
onMount() {
  this.root.append(this.buildTenThousandRows())
}

patch(i, value) {
  this.state.cells[i].textContent = value
}
10k rowsdirect textContent updates at vanilla speed
external DOM libraries use the same pattern: mount once, update their nodes directly

10 / 10

Navigation and tooling

The distribution includes progressive page navigation, a component compiler and focused developer tools. They are optional, but they share the same runtime and component model.

Fez.pjax · boot gating · start()

Navigation only when invited

Fez.load, Fez.refresh and Fez.pjax are always exposed, but click, popstate and form handlers bind only when the initial page contains a named pjax container. Late containers opt in with Fez.pjax.start().

<main id="pjax" class="pjax">
  ...server-rendered page content...
</main>

// late-injected container
Fez.pjax.start()

// pages without a pjax container keep
// ordinary browser navigation
container foundlinks, history and forms enhanced
there is no window.Pjax global - everything lives on Fez

Fez.load · Fez.refresh · one signature, different defaults

Load and refresh

Both take a URL, a #selector or an element, plus target, source, form, history and scroll. refresh is the fresh variant: no-cache, no double-click debounce and no scroll. Defaults fill only what you leave out, and the Promise never rejects.

Fez.load('/users')
Fez.load('#panel')
Fez.load('/users', { target: '#panel' })
Fez.refresh()
Fez.refresh('#sidebar')

const detail = await Fez.load('/users')
detail?.status   // 200
detail?.error    // null | 'status' | 'network' | ...

Fez.qs('page', 4)
Fez.hash('tab', 'settings')
load: cached, debounced, scrolls on a full swaprefresh: no-cache, never debounced, keeps scroll
loads into different nodes run side by side; a full load cancels them

links · forms · target · refresh · confirm · replace

Declarative navigation attributes

Ordinary links are intercepted automatically. Classes opt out, target attributes replace only one region, refresh attributes re-fetch the current URL, confirmation can be synchronous or async, and forms submit through pjax.

<a href="/users">Users</a>
<a href="/export" class="no-pjax">Download</a>
<a href="/tab" pjax-target="#panel">Tab</a>
<button pjax-refresh="#panel">Refresh</button>
<a href="/delete" pjax-confirm="Are you sure?">Delete</a>
<a href="/filters" pjax-replace>Filters</a>

<form data-pjax="true">...</form>
<form data-pjax="#panel">...</form>
full swaptarget swapnative opt-outpjax form
the direct class is also accepted as a native-navigation opt-out

pjax:start · pjax:render · before · after · confirm · emit

Cancelable hooks and lifecycle events

Cancel before navigation, customize confirmation, react after full swaps and listen for structured start and render events. Render detail includes origin, destination, status, error, duration, mode and options.

document.addEventListener('pjax:start', event => {})
document.addEventListener('pjax:render', event => {
  console.log(event.detail.from, event.detail.to)
  console.log(event.detail.status, event.detail.error)
  console.log(event.detail.duration, event.detail.mode)
})

Fez.pjax.before = (href, opts) => true
Fez.pjax.after = href => analytics.page(href)
Fez.pjax.confirm = (message, node) => Dialog.confirm(message)
Fez.pjax.emit('app:navigated', { href: Fez.pjax.path() })
start cancelablerender measuredasync confirm
inside a component, this.on('pjax:render', handler) cleans itself up

Fez.pjax.config · View Transitions · response scripts

Configurable page swaps

Configure silent mode, skip paths, no-scroll and no-AJAX selectors, timeout and history cache size. Native View Transitions are optional. Response scripts run before morph unless marked pjax-delay.

Fez.pjax.config.timeout = 12_000
Fez.pjax.config.history_max = 40
Fez.pjax.config.paths_to_skip.push(/^\/admin\/export/)
Fez.pjax.config.no_scroll_selector.push('.keep-scroll')
Fez.pjax.config.no_pjax_class.push('native-link')
Fez.pjax.config.no_ajax_class.push('native-form')

Fez.pjax.useViewTransition = true

<script pjax-delay>afterSwap()</script>
policy configuredhistory cachedview transition optional
Fez.pjax.error and Fez.pjax.console can be replaced for application reporting

compile · template · refactor · index · debug · agents · version

Focused CLI tools

Validate complete files or templates, inspect generated template code, report modernization candidates, removed APIs, // comments in styles and legacy Pjax. calls, index component metadata, open a Playwright debugging REPL, print or initialize the agent reference, and print the version.

fez compile src/fez/ui-card.fez
fez compile 'src/fez/**/*.fez'
fez template src/fez/ui-card.fez
fez compile --debug-template src/fez/ui-card.fez
fez template --debug src/fez/ui-card.fez

fez refactor src/fez
fez index src/fez
fez debug http://localhost:8000/index.html
fez agents
fez agents --init
fez version
fez --help
syntax checkedtemplate checkedmetadata indexedbrowser debugged
the CLI uses the same compiler as the browser runtime

fez.d.ts · AGENTS.md

Typed and agent-readable interfaces

The package ships TypeScript declarations for component state, props, hooks, helpers, transitions and page navigation. The complete Fez-specific authoring reference fits in AGENTS.md for coding tools and humans.

/// <reference path="./node_modules/@dinoreic/fez/fez.d.ts" />

/** @type {FezBase} */
const dialog = Fez('#dialog')

/** @type {PjaxLoadOptions} */
const options = {
  target: '#panel',
  history: false,
  scroll: false,
}

// Agent reference
// fez agents --init
editor autocompleteAPI checkingLLM reference
plain JavaScript projects gain tooling without adding a compile step

next-step

Use one feature or the whole kit.

Start with a single component, inspect complete working examples, or keep the full reference open while you build.