<style> & > div { border: 3px solid #bb8; padding: 10px; border-radius: 8px; background-color: #ffe; margin-bottom: 16px; } </style> <div> {{#if props.small}} <p> <small>{{ props.small }}</small> </p> {{/if}} <slot /> </div> <h1 class="jumbo" style="margin: 70px auto;">&darr;</h1> <!-- you can use {{ or [[ for template blocks --> [[#if props.title]] <h1>[[ props.title ]]</h1> [[/if]]

Fez → reactive components

directly in HTML with zero build steps

exampleslibGitHub repoAI GuideBenchmark

<html> <head> <title>Time</title> <script script="https://dux.github.io/fez/dist/fez.js"></script> <script script="/fez/ui-time.fez"></script> </head> <body> <ui-time city="My place"></ui-time> <body> </html>

Fez brings a familiar component-based approach you know from Vue or React, but strips away the build complexity. It lets you write components naturally, right in your HTML, without needing a compilation step. Think of it as a jQuery for modern component development, with the simplicity of dropping a script tag into your page. You get to use your components as if they were native HTML elements, making the development experience straightforward and intuitive.

What does Claude AI think about Fez?"

Prompt
Look at FEZ_GUIDE.md, README.md and components in fez/*.fez, then write what do you think of the lib. Is it bette / worse / different then react / vue / svelte. When should one chose Fez or maybe never.
Response

Final Verdict

Fez is a hidden gem that challenges the complexity of modern frameworks. It proves you can have reactivity, components, and modern DX without webpack configs, transpilers, or framework lock-in. It's particularly brilliant for developers who miss the simplicity of jQuery but want modern component architecture. While it won't replace React for large enterprise apps, it's perfect for the 80% of web projects that don't need that complexity.

With reactive state and custom style.

  1. Add Fez JS (to HEAD) <script src="https://dux.github.io/fez/dist/fez.js"></script>
  2. Create ui-counter component <template fez="ui-counter"> <script> init(props) { // called when Fez node is connected to DOM this.MAX = 6 this.state.count = parseInt(props.start || 0) } isMax() { // is state is changed, template is re-rendered return this.state.count >= this.MAX } more() { this.state.count += this.isMax() ? 0 : 1 } </script> <style> /* compiles from scss to css and injects class in head */ /* body style */ background-color: #f7f7f7; /* scoped to this component */ :fez { margin: 10px 0; button { position: relative; top: -3px; } span { padding: 5px 10px; } } </style> <button class="btn-sm" onclick="fez.state.count -= 1" disabled={{ state.count < 1 }}>-</button> <span>{{ state.count }}</span> <button class="btn-sm" onclick="fez.more()" disabled={{ isMax() }}>+</button> {{if state.count > 0}} <span>&amp;mdash;</span> {{if state.count == MAX }} MAX {{else}} {{if state.count % 2 }} odd {{else}} even {{/if}} {{/if}} {{/if}}

    Or load via HTTP (as in this case)

    <script fez="/demo/ui-counter.fez"></script>
  3. Place ui-counter anywhere in BODY <div id="counters"> <ui-counter></ui-counter> <ui-counter start="1"></ui-counter> <ui-counter start="2"></ui-counter> </div>
  4. That is it! Add then as regular DOM nodes.

You will learn all Fez features, just by inspecting these examples. You can edit and update both HTML and Fez code.

open in CodePen

Demo: input-color

Basic Usage
With Change Handler
Color Preview
Disabled State
  • Fires change events and supports onchange callbacks
  • Features: onPropsChange() value monitor.
  • Supports value attribute for initial color and disabled attribute
<table> <tr> <td> <b>Basic Usage</b> </td> <td> <div class="demo-row"> <input-color value="#4A90E2"></input-color> </div> </td> </tr> <tr> <td> <b>With Change Handler</b> </td> <td> <div class="demo-row"> <input-color value="#FF6B6B" :onchange="updateBackground"></input-color> <span style="padding:7px 10px; border-radius: 6px; position: relative; top: -5px; left: 10px;" id="preview">Color Preview</span> </div> </td> </tr> <tr> <td> <b>Disabled State</b> </td> <td> <div class="demo-row"> <input-color value="#95A5A6" disabled="true"></input-color> </div> </td> </tr> </table> <script> function updateBackground(color) { document.getElementById('preview').style.backgroundColor = color; } </script>
<script> updateColor(e) { this.state.showPicker = false this.setColor(e.target.value) } setColor(value) { this.state.color = value if (this.props.onchange) { this.props.onchange(this.state.color) } } togglePicker() { this.state.showPicker = !this.state.showPicker } closePicker(e) { if (!this.root.contains(e.target)) { this.state.showPicker = false } } onPropsChange(name, value) { if (name == 'value') { this.setColor(value) } } onMount(props) { this.setColor(props.value || '#000000') this.state.showPicker = false } </script> <style> :fez { display: inline-block; position: relative; font-family: system-ui, -apple-system, sans-serif; .color-input-wrapper { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; transition: border-color 0.2s; &:hover { border-color: #999; } &:focus-within { border-color: #4A90E2; outline: 2px solid rgba(74, 144, 226, 0.2); outline-offset: 1px; } } .color-preview { width: 24px; height: 24px; border-radius: 4px; border: 1px solid rgba(0,0,0,0.1); box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2); } .color-value { font-size: 14px; color: #333; font-family: monospace; user-select: none; } .color-picker-popup { position: absolute; top: calc(100% + 4px); left: 0; z-index: 1000; background: white; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); padding: 8px; min-width: 200px; input[type="color"] { width: 100%; height: 150px; border: none; border-radius: 4px; cursor: pointer; padding: 0; &::-webkit-color-swatch-wrapper { padding: 0; } &::-webkit-color-swatch { border: none; border-radius: 4px; } } } &.disabled { .color-input-wrapper { background: #f5f5f5; cursor: not-allowed; opacity: 0.6; &:hover { border-color: #ddd; } } } } </style> <div class="color-input-wrapper {{ props.disabled ? 'disabled' : '' }}" onclick="fez.togglePicker()"> <div class="color-preview" style="background-color: {{ state.color }}"></div> <span class="color-value">{{ state.color }}</span> </div> <div class="color-picker-popup" style="display: {{ state.showPicker && !props.disabled ? 'block' : 'none' }};"> <input name={{ props.name }} type="color" value="{{ state.color }}" onchange="fez.updateColor(event)" onclick="event.stopPropagation()" /> </div>
open in CodePen

Demo: ui-avatar

  • features template blocks {{block ...}}
<style> .avatars { img.avatar { border: 2px solid #ddd; background-color: #fff; margin-right: -10px; } } </style> <div class="avatars"> <ui-avatar size="50" name="Dux"></ui-avatar> <ui-avatar size="55" name="Mile"></ui-avatar> <ui-avatar size="60" src="https://robohash.org/c.png?set=set2"></ui-avatar> <ui-avatar size="65" src="https://robohash.org/d.png?set=set2"></ui-avatar> <ui-avatar size="70" src="https://robohash.org/e.png" name="Joza"></ui-avatar> </div>
<script> NAME = 'span' FAST = true init(props) { this.copy('href', 'style') this.size = (props.size || 64) + 'px' if (props.name) { props.src ||= `https://robohash.org/${escape(props.name)}.png` } } </script> <style> img { border-radius: 50%; } span.avatar { display: inline-block; text-align: center; .title { display: inline-block; border: 1px solid #ccc; background-color: #eee; padding: 1px 5px; font-size: 13px; position: relative; left: 3px; top: 8px; border-radius: 4px; } } </style> {{block avatar}} <img src={{ props.src }} class="avatar" style="width: {{ size }}; height: {{ size }};" /> {{/block}} {{if props.name}} <span class="avatar"> <div class="title">{{ props.name }}</div> <div> {{block:avatar}} </div> </span> {{else}} {{block:avatar}} {{/if}}
open in CodePen

Demo: ui-border

this will get border from fez component. Also check the DOM, fez parent is removed from DOM.
  • Features: internal method this.fezHide() will remove fez node from dom, and set this.root to first parent node and return initial child nodes.
  • useful if you need to transform children, but you can't have parent node in place. Think jQuery plugin.
<div class="should-be-first-parent"> <ui-border color="violet"> <div style="padding: 20px; max-width: 200px;"> this will get border from fez component. Also check the DOM, fez parent is removed from DOM. </div> </ui-border> </div>
<script> onMount() { const childNodes = this.fezHide() childNodes.forEach(node => { node.style.border = `3px solid ${this.props.color || 'black'}` }) } </script>
open in CodePen

Demo: ui-card



The magician

Click to flip!

The high
priestess

Click to flip!

The empress

Click to flip!

Another clock?

Click to see

Any HTML or Fez component inside.
  • Simple UI component
  • Pub/sub for card flip
  • Direct component access (node.fez)
  • Slot - pass child nodes
<script> function randomFlip() { let timeout = 150 let current = 0 document.querySelectorAll('.fez-ui-card').forEach(n => { setTimeout(() => n.fez.flip(), current += timeout) setTimeout(() => n.fez.flip(), current + 1300) }) } function flipAllCards() { document.querySelectorAll('.fez-ui-card').forEach(n => n.fez.flip()) } </script> <button onclick="randomFlip()">Random flip</button> <button onclick="flipAllCards()">Flip cards via direct access</button> <button onclick="Fez.publish('flip-card')">Flip cards via pub/sub</button> <br /> <br /> <ui-card> <div class="front"> <h2>The magician</h2> <p>Click to flip!</p> </div> <div class="back" style="background: url('https://tarotatlas.com/wp-content/themes/ta/img/m1.webp') no-repeat center/cover;"> </div> </ui-card> <ui-card> <div class="front"> <h2>The high<br />priestess</h2> <p>Click to flip!</p> </div> <div class="back" style="background: url('https://tarotatlas.com/wp-content/themes/ta/img/m2.webp') no-repeat center/cover;"> </div> </ui-card> <ui-card> <div class="front"> <h2>The empress</h2> <p>Click to flip!</p> </div> <div class="back" style="background: url('https://tarotatlas.com/wp-content/themes/ta/img/m3.webp') no-repeat center/cover;"> </div> </ui-card> <ui-card> <div class="front"> <h2>Another clock?</h2> <p>Click to see</p> </div> <div class="back" style="background-color: #aba;"> <div style="width: 150px;"> <ui-clock></ui-clock> </div> <div style="padding: 30px;"> Any HTML or Fez component inside. </div> </div> </ui-card>
<script> flip() { this.find('.card').classList.toggle('flipped') } autoflip() { setTimeout(()=>{ this.flip() this.autoflip() }, 5000 + (Math.random() * 15* 1000)) } init() { this.autoflip() this.subscribe('flip-card', this.flip) } </script> <style> .card { display: inline-block; float: left; width: 250px; height: 400px; perspective: 1000px; cursor: pointer; margin: 10px; } .fez-slot { position: relative; width: 100%; height: 100%; text-align: center; transition: transform 0.8s; transform-style: preserve-3d; } .card.flipped .fez-slot { transform: rotateY(180deg); } .front, .back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; display: flex; flex-direction: column; justify-content: center; align-items: center; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); } .front { background: #fff; color: #333; } .back { background: #fff; transform: rotateY(180deg); } .card:hover { transform: scale(1.02); transition: transform 0.3s ease; } </style> <div class="card" onclick="fez.flip()"> <slot /> </div>
open in CodePen

Demo: ui-clock

  • SVG generation
  • Features: reactive store - any update to this.state object triggers re-render.
  • Features: this.setStyle(key, value) - set style to base node
  • Features: DOM morph updates - update only changed nodes and attributes
  • Features: onDestroy() { ... } - execute code when component is removed from DOM
  • Features: setInterval() { ... } to set interval on a instance. No need to clear it, it will be auto cleared on component destroy.
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;"> <div> <ui-clock city="Zagreb" utc="1"></ui-clock> </div> <div> <ui-clock city="Moscow" utc="4"></ui-clock> </div> <div> <ui-clock city="NYC" utc="-5"></ui-clock> </div> <div> <ui-clock city="Sydney" utc="10"></ui-clock> </div> </div>
<script> randomColor() { const colors = ['red', 'green', 'blue', 'magenta', 'teal', 'orange', 'violet'] return colors[Math.floor(Math.random() * colors.length)]; } setVars() { // will only render once on next tick let time = new Date(Date.now() - (this.offsetHours * 60 * 60 * 1000)) this.state.hours = time.getHours() this.state.minutes = time.getMinutes() this.state.seconds = time.getSeconds() this.setStyle('--color', this.randomColor()) if (this.props.publish) { Fez.publish('clock-second', this.state.seconds) } } init(props) { this.offsetHours = parseFloat(this.props.utc || 0) this.setVars() this.setInterval(this.setVars, 1000) } onDestroy() { console.log(`Bye from ${this.fezName} - ${this.props.city}`) } </script> <style> input { border: 3px solid red !important; } svg { width: 100%; height: 100%; } .clock-face { stroke: #333; fill: white; } .minor { stroke: #999; stroke-width: 0.5; } .major { stroke: #333; stroke-width: 1; } .hour { stroke: #333; stroke-width: 1.5; } .minute { stroke: #666; } .second, .second-counterweight { stroke: var(--color); } .second-counterweight { stroke-width: 3; } </style> <svg viewBox="-50 -50 100 100"> <circle class="clock-face" r="48" /> <!-- markers --> {{each [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] as minute}} <line class="major" y1="35" y2="45" transform="rotate({{30 * minute}})" /> {{each [1, 2, 3, 4] as offset}} <line class="minor" y1="42" y2="45" transform="rotate({{6 * (minute + offset)}})" /> {{/each}} {{/each}} <!-- hour hand --> <line class="hour" y1="2" y2="-20" transform="rotate({{30 * state.hours + state.minutes / 2}})" /> <!-- minute hand --> <line class="minute" y1="4" y2="-30" transform="rotate({{6 * state.minutes + state.seconds / 10}})" /> <!-- second hand --> <g transform="rotate({{6 * state.seconds}})"> <line class="second" y1="10" y2="-38" /> <line class="second-counterweight" y1="10" y2="2" /> </g> <!-- City text --> {{if props.city }} <text x="0" y="-17" text-anchor="middle" dominant-baseline="middle" style="font-size: 10px; fill: #111;"> {{ props.city }} </text> {{/if}} </svg>
open in CodePen

Demo: ui-confetti

  • Features: module imoorts
<ui-confetti> <button>Congratulations!</button> </ui-confetti> ⋅ <ui-confetti> <button>And more congratulations!</button> </ui-confetti>
<script> import confetti from 'https://cdn.skypack.dev/canvas-confetti'; class { NAME = 'span' init() { this.root.onclick = ()=>confetti() } } </script>
open in CodePen

Demo: ui-counter

  • disabled state - attribute removed when value is false
  • access current Fez component instance via fez. pointer
  • global and local CSS
<ui-counter></ui-counter>
<script> init(props) { // called when Fez node is connected to DOM this.MAX = 6 this.state.count = parseInt(props.start || 0) } isMax() { // is state is changed, template is re-rendered return this.state.count >= this.MAX } more() { this.state.count += this.isMax() ? 0 : 1 } </script> <style> /* compiles from scss to css and injects class in head */ /* body style */ background-color: #f7f7f7; /* scoped to this component */ :fez { margin: 10px 0; button { position: relative; top: -3px; } span { padding: 5px 10px; } } </style> <button class="btn-sm" onclick="fez.state.count -= 1" disabled={{ state.count < 1 }}>-</button> <span>{{ state.count }}</span> <button class="btn-sm" onclick="fez.more()" disabled={{ isMax() }}>+</button> {{if state.count > 0}} <span>—</span> {{if state.count == MAX }} MAX {{else}} {{if state.count % 2 }} odd {{else}} even {{/if}} {{/if}} {{/if}}
open in CodePen

Demo: ui-dialog

<script> funcPass() { this.props.func( Math.random() ) } </script> <button onclick="fez.funcPass()">Test func pointer pass</button>
  • Features: global handler
  • Features: Publish subscribe. Check code to see how easy it to enable / disable action buttons in Dialog.
<button onclick="Dialog.show('#dialog-data')">Open dialog</button> <template id="dialog-data"> <h1>Nice clock</h1> <div style="max-width: 300px; margin: auto;"> <ui-clock publish="true"></ui-clock> </div> <div style="text-align: right; margin-top: 20px;"> On seconds 0,1, and 2 button is enabled. → <button class="action-button" onclick="Toast.info('Dont click so hard!')" >Action</button> </div> </template> <xmp fez="tmp-test-attr"> <script> funcPass() { this.props.func( Math.random() ) } </script> <button onclick="fez.funcPass()">Test func pointer pass</button> </xmp>
<script> // if GLOBAL is present, Fez engine // * on document load auto appends component to document // * after init() runs window[GLOBAL] = fez (mounted component) // this is singleton pattern GLOBAL = 'Dialog' close() { this.state.data = null } show(data) { if (data) { if (data[0] == '#') { data = document.getElementById(data.replace('#', '')).innerHTML } this.state.data = data } } // Dialog.actionButton(true | false) -> enable | disable // Just add a button with class 'action-button' actionButton(value) { const button = this.find('.action-button') if (button) { if (value) { button.removeAttribute('disabled') } else { button.setAttribute('disabled', '') } } } testFunc(num) { Toast.info(`Passed: ${num}`) } init() { // for demo purposes, we subscribe to global event this.subscribe('clock-second', (val)=>{ this.actionButton(val % 6 < 3) }) } </script> <style> .global-dialog { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; .dialog-content { background: white; border-radius: 8px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); padding: 2rem; margin: 100px auto; max-width: 600px; max-height: calc(100vh - 200px); overflow-y: auto; position: relative; h1, h2 { margin-top: 0px; text-align: center; } } .dialog-close { position: absolute; top: 15px; right: 15px; } } </style> <div class="global-dialog" onclick="fez.close()" style="display: {{ state.data ? 'block' : 'none' }};"> <div class="dialog-content" onclick="event.stopPropagation()"> <button class="dialog-close" onclick="fez.close()">×</button> {{raw state.data }} <center style="padding-top: 30px;"> <tmp-test-attr :func="fez.testFunc"></tmp-test-attr> </center> </div> </div>
open in CodePen

Demo: ui-editor

Hi, I am UI-EDITOR component. To define me, just add <ui-editor></ui-editor> anywhere in HTML code
  • this is viewer / editor you see on the right
  • Features: Fez.head(...) that will safely insert script tags in the page header
  • Features: Fez.untilTrue(...) that will execute code every 100 milliseconds until it returns true
<ui-editor file="demo.html"> Hi, I am UI-EDITOR component. To define me, just add <ui-editor></ui-editor> anywhere in HTML code </ui-editor>
<head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/vs2015.min.css" /> </head> <script> import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js'; class { reformatIndentation(text) { const lines = text.split('\n'); if (lines.length < 2) return text; let baseIndent = 0; for (let i = 1; i < lines.length; i++){ const line = lines[i].trim(); if (line.length > 0) { baseIndent = lines[i].match(/^\s*/)[0].length; break; } } const targetIndent = Math.max(0, baseIndent - 2); const processedLines = lines.map(line => { const lineWithSpaces = line.replace(/\t/g, ' '); const leadingSpaces = lineWithSpaces.match(/^\s*/)[0].length; if (leadingSpaces === 0) return line; const newIndent = Math.max(0, leadingSpaces - targetIndent); return ' '.repeat(newIndent) + lineWithSpaces.trim(); }); return processedLines.join('\n'); } getSource() { return this.codeNode.textContent } copy() { try { navigator.clipboard.writeText(this.getSource()); Toast.info('File data copied to clipboard.'); } catch (err) { console.error('Failed to copy: ', err); Toast.error('Failed to copy to clipboard.'); } } init(props) { props.language ||= 'html' const child = this.root.firstElementChild if (child?.nodeName == 'TEMPLATE' || child?.nodeName == 'XMP') { this.source = Fez.htmlEscape(this.root.firstElementChild.innerHTML) } else { this.source = this.root.innerHTML } } onMount() { const node = this.codeNode = this.find('code') node.innerHTML = this.reformatIndentation(this.source.trim()) hljs.highlightElement(node) } } </script> <style> code { padding: 10px; font-size: 15px; } pre { margin-top: 20px; width: 100%; line-height: 26px; } </style> {{#if props.file}} <div style="margin: 0; position: relative; top: 7px;"> <a href="https://raw.githubusercontent.com/dux/fez/main/{{ props.file.replace('./', '') }}" target="raw" >{{ props.file }}</a> ⋅ <button class="btn-sm" onclick="fez.copy()">Copy</button> {{#if props.action }} ⋅ <button class="btn-sm" style="cursor: pointer; font-weight: 600;" onclick="fez.props.action('{{ props.name }}')" >Update</button> {{/if}} </div> {{/if}} <pre> <code class="language-{{ props.language }}" contenteditable="{{ !!props.file }}"></code> </pre>
open in CodePen

Demo: ui-fa


Brand Icons

Regular Icons

  • Font Awesome icon component with name parameter
  • Supports dynamic size and color props, via onPropsChange
  • evaluate value of a attribute if prefixed with column : (in this example :size=
  • Accepts full Font Awesome class names (e.g., "fab fa-github") or shorthand (e.g., "home")
  • Supports regular attribute for regular icons (e.g., regular="user" = "far fa-user")
  • Supports brand attribute for brand icons (e.g., brand="github" = "fab fa-github")
  • Dynamic size control via range input
<input type="range" min="16" max="64" class="slider" id="fa_range" value="24" oninput="s = this.value; Fez('ui-fa', (el) => el.root.setAttribute('size', s) )" /> <br /> <ui-fa name="home" :size="document.getElementById('fa_range').value"></ui-fa> <ui-fa name="heart" color="red"></ui-fa> <ui-fa name="star" color="gold"></ui-fa> <ui-fa name="cog" color="blue"></ui-fa> <h4>Brand Icons</h4> <ui-fa name="fab fa-github" size="32"></ui-fa> <ui-fa brand="twitter" color="#1DA1F2" size="32"></ui-fa> <ui-fa brand="facebook" color="#1877F2" size="32"></ui-fa> <ui-fa brand="instagram" color="#E4405F" size="32"></ui-fa> <h4>Regular Icons</h4> <ui-fa name="far fa-user" size="24"></ui-fa> <ui-fa regular="heart" color="red" size="24"></ui-fa> <ui-fa regular="star" color="gold" size="24"></ui-fa> <script>setTimeout(() => Fez('ui-fa', (el) => el.setSize(fa_range.value)), 500)</script>
<script> Fez.head({css: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css'}) class { NAME = 'i' setSize(size) { if (size) { this.root.style.fontSize = `${parseInt(size)}px` } } setColor(value) { if (value) { this.setStyle('color', value) } } onPropsChange(name, value) { if (name == 'color') { this.setColor(value) } if (name == 'size') { this.setSize(value) } } setIcon(name) { // Remove existing FA classes this.root.className = this.root.className.replace(/\bfa[a-z]?\s+fa-[\w-]+/g, '') // Parse icon name - supports formats like "fas fa-home" or just "home" if (name.includes(' ')) { this.root.className += ` ${name}` } else { // Default to solid style this.root.className += ` fas fa-${name}` } } init(props) { this.copy('onclick', 'title') if (props.name) { this.setIcon(props.name) } else if (props.regular) { this.setIcon(`far fa-${props.regular}`) } else if (props.brand) { this.setIcon(`fab fa-${props.brand}`) } this.setColor(props.color) this.setSize(props.size) } } </script> <style> display: inline-block; line-height: 1; transition: font-size 0.2s ease; </style>
open in CodePen

Demo: ui-fetch

  • Automatic JSON parsing for responses
  • Session-based caching (cache is cleared on page refresh)
  • Support for both promise-based and callback-based usage
  • HTTP method support (GET, POST, etc.)
  • Integrated error handling via Fez.onError
  • Optional logging via Fez.LOG
<ui-fetch></ui-fetch>
<script> init() { this.state.results = { promise: '', callback: '', post: '' } this.state.loading = false this.state.logEnabled = Fez.LOG } async fetchData() { this.state.loading = true const url = 'https://jsonplaceholder.typicode.com/users/1' // Promise-based fetch const data = await Fez.fetch(url) this.state.results.promise = JSON.stringify(data, null, 2) // Callback-based fetch (will hit cache) Fez.fetch(url, (data) => { this.state.results.callback = JSON.stringify(data, null, 2) }) // POST request const postData = await Fez.fetch('POST', 'https://jsonplaceholder.typicode.com/posts', { title: 'Test Post', body: 'This is a test post', userId: 1 }) this.state.results.post = JSON.stringify(postData, null, 2) this.state.loading = false } clearCache() { Fez._fetchCache = {} this.state.results = { promise: '', callback: '', post: '' } } toggleLogging() { Fez.LOG = !Fez.LOG this.state.logEnabled = Fez.LOG } </script> <style> :fez { padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); h2 { margin-top: 0; color: #333; } .controls { margin: 20px 0; button { margin-right: 10px; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; transition: background 0.2s; &:hover { background: #0056b3; } &:disabled { background: #ccc; cursor: not-allowed; } } } .status { margin: 10px 0; padding: 10px; background: #f0f0f0; border-radius: 4px; font-family: monospace; } pre { background: #f8f9fa; padding: 12px; border-radius: 4px; overflow-x: auto; min-height: 50px; border: 1px solid #dee2e6; font-size: 13px; line-height: 1.5; } h3 { margin-top: 24px; margin-bottom: 12px; color: #495057; } .hint { color: #6c757d; font-size: 14px; font-style: italic; } } </style> <h2>Fez.fetch Demo</h2> <div class="status"> Logging: <strong>{{ state.logEnabled ? 'Enabled' : 'Disabled' }}</strong> {{if state.logEnabled}} (check console for fetch logs) {{/if}} </div> <div class="controls"> <button onclick="fez.fetchData()" disabled={{ state.loading }}> {{ state.loading ? 'Loading...' : 'Fetch Data' }} </button> <button onclick="fez.clearCache()">Clear Cache</button> <button onclick="fez.toggleLogging()"> {{ state.logEnabled ? 'Disable' : 'Enable' }} Logging </button> </div> {{if state.results.promise || state.results.callback || state.results.post}} <div class="results"> {{if state.results.promise}} <h3>Promise-based GET:</h3> <pre>{{ state.results.promise }}</pre> {{/if}} {{if state.results.callback}} <h3>Callback-based GET:</h3> <p class="hint">Same URL - should hit cache on second click</p> <pre>{{ state.results.callback }}</pre> {{/if}} {{if state.results.post}} <h3>POST Request:</h3> <pre>{{ state.results.post }}</pre> {{/if}} </div> {{else}} <p class="hint">Click "Fetch Data" to see the results</p> {{/if}}
open in CodePen

Demo: ui-form





    
  • Features: form helper - this.formData(). Get form data as object.
  • Features: form helper - this.onSubmit(). If present, auto bind form and get form data as object
  • prefix params with : if you want to calculate attribute value. Same as in Vue, current example :ping="..."
  • Custom DOM tag name - FORM instead of default DIV
<div class="flex"> <ui-form target="/api" :ping="updateFormData"> <p> <input type="text" name="info" value="a dude" /> </p> <p> <select name="num"> <option>one</option> <option>two</option> <option>three</option> </select> </p> <p> <label><input type="radio" name="name" value="Jakov" /> Jakov</label> <label><input type="radio" name="name" value="Vid" /> Vid</label> <label><input type="radio" name="name" value="Dino" /> Dino</label> </p> <p> <button>Submit</button> </p> </ui-form> <ui-form target="/api" :ping="updateFormData"> <p> <select name="num"> <option>uno</option> <option>due</option> <option>tres</option> </select> </p> <p> <input-color name="color" value="#abcdef" /> </p> <p> <button>Submit</button> </p> </ui-form> </div> <pre id="form-data"></pre> <script> function updateFormData(obj) { document.getElementById('form-data').textContent += JSON.stringify(obj) + "\n" } </script>
<script> NAME = 'form' // if you define onSubmit, you will get submited form data object // and event.preventDefault will allready be applied onSubmit(data) { this.props.ping(data) } </script> <style> border: 2px solid green; border-radius: 5px; padding: 15px; margin: 15px 0; background-color: #efe; label { display: block; cursor: pointer; margin-bottom: 5px; } select option { font-size: 16px; } </style>
open in CodePen

Demo: ui-markdown

### Hi I am **markdown** from Fez custom component called ``, created inline in HTML. * Fez is **too simple** to be true! * `index.html` SPA * New way to write web apps, without the bloat. * See this example on [JSbin](https://jsbin.com/dopuram/edit?html,output)

I am regular HTML

`Fez` md **again!** :)
  • Features: Module imports
<ui-markdown> ### Hi I am **markdown** from Fez custom component called `<ui-markdown>`, created inline in HTML. * Fez is **too simple** to be true! * `index.html` SPA * New way to write web apps, without the bloat. * See this example on [JSbin](https://jsbin.com/dopuram/edit?html,output) </ui-markdown> <h3>I am regular HTML</h3> <ui-markdown>`Fez` md **again!** :)</ui-markdown>
<script> import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js' class { init(props) { let text = props.html || this.root.innerHTML; this.root.classList.add('markdown'); this.root.innerHTML = marked.parse(text); } } </script>
open in CodePen

Demo: ui-pub-sub

⋅ Fast ping:

listener 1

listener 2

listener 3

Foo:

  • Features: publish <> subscribe mechanism. Auto disconnect on node destroy.
  • Direct Fez component creation via Fez(tagName, class { ... })
<style> .fez-ui-pubsub { margin-top: 10px; } </style> <button onclick="Fez.publish('ping', Math.random())">ping from anywhere</button> ⋅ Fast ping: <button onclick=" clearInterval(window.fastPing); window.fastPing = setInterval(()=>Fez.publish('ping', Math.random()), 10);" >START</button> ⋅ <button onclick="clearInterval(window.fastPing)">END</button> <ui-pubsub> <h4>listener 1</h4> <p class="target"></p> </ui-pubsub> <ui-pubsub> <h4>listener 2</h4> <b class="target"></b> </ui-pubsub> <ui-pubsub> <h4>listener 3</h4> Foo: <span class="target"></span> </ui-pubsub> <template id="pubsub-t"> <ui-pubsub> Dynamic listener: <button onclick="Fez(this).root.remove()">×</button> ⋅ <span class="target"></span> </ui-pubsub> </template> <br /> <button onclick="this.insertAdjacentHTML('afterend', document.getElementById('pubsub-t').innerHTML)">add listener</button> <script> Fez('ui-pubsub', class { update (info) { this.target.innerHTML = info } init() { this.target = this.find('.target') this.subscribe('ping', this.update) this.update('waiting for a ping...') } }) setInterval(()=>Fez.publish('ping', Math.random()), 1000) </script>
<--! defined in HTML block -->
open in CodePen

Demo: ui-slider

Slide 1

Slide 2

Slide 3

Slide 4

  • Add any HTML

Slide 5

  • sweet component with css animations :)
  • Features: this.find() to find node in template, shortcut for this.root.querySelector()
  • Features: this.beforeRender() to prepare vars before render (clean alternative to svelte $: {...}
  • Features: this.onResize() to execute code on window resized. Auto cleared when node is removed.
<style> .fez-c-slider { .fez-slot { > div { text-align: center; } } } div.img { img { width: 100%; max-height: 600px; object-fit: cover; border-radius: 8px; } } </style> <ui-slider> <div> <h1>Slide 1</h1> <div class="img"> <img src="https://images.unsplash.com/photo-1737143765999-bd3be790ab4f?w=600" /> </div> </div> <div> <h1>Slide 2</h1> <div class="img flex"> <img src="https://images.unsplash.com/photo-1735767975829-71496633d499?w=600" /> <img src="https://images.unsplash.com/photo-1736158064402-5b68c2cbcc77?w=600" /> </div> <div style="width: 200px; margin: 20px auto;"> <ui-clock city="Slider!"></ui-clock> </div> </div> <div> <h1>Slide 3</h1> <div class="img"> <img src="https://images.unsplash.com/photo-1737898415581-7dea57a1905b?w=600" /> </div> </div> <div> <h1>Slide 4</h1> <div class="img"> <img src="https://images.unsplash.com/photo-1736158064402-5b68c2cbcc77?w=600" /> </div> <ul> <li>Add any HTML</li> </ul> </div> <div> <h1>Slide 5</h1> <div class="img"> <img src="https://images.unsplash.com/photo-1736185669686-f302d6274f23?w=600" /> </div> </div> </ui-slider>
<script> setSlide(num) { this.state.slide = num if (this.state.slide < 0) { this.state.slide = this.slidesCount - 1 } else if (this.state.slide >= this.slidesCount) { this.state.slide = 0 } const width = this.find('.slot')?.getBoundingClientRect().width ?? 0 this.setStyle('--offset', `-${width * this.state.slide}px`) } changeSlide(direction){ this.setSlide( this.state.slide += direction ) } init() { this.slidesCount = this.childNodes().length this.setStyle('--arrow-width', `70px`) this.setSlide(0) this.onResize(this.render, 100) } </script> <style> table.slides { width: 100%; table-layout: fixed; td { &.arrow { cursor: pointer; width: var(--arrow-width); div { display: flex; justify-content: center; align-items: center; font-size: 50px; color: #aaa; width: var(--arrow-width); span { transform: rotate(-90deg); } } &:hover span { color: #111; } &:nth-child(3) { span { transform: rotate(90deg) translateY(10px); } } } div.slot { overflow: hidden; display: flex; align-items: flex-start; max-width: 100%; .slot-parent { max-width: 100%; transition: transform 0.3s ease; transform: translateX(var(--offset)); .fez-slot { display: flex; & > div { width: 100%; flex-shrink: 0; } } } } } } </style> <table class="slides"> <tr> <td class="arrow" onclick="fez.changeSlide(-1)"> <div> <span>⇧</span> </div> </td> <td> <div class="slot"> <div class="slot-parent"> <slot /> </div> </div> </td> <td class="arrow" onclick="fez.changeSlide(1)"> <div> <span>⇧</span> </div> </td> </tr> </table>
open in CodePen

Demo: ui-style

This text will be smaller on mobile. Using :mobile { ... shortcut.
  • Features: Fez.styleMacro - define style shortcuts.
<ui-style>This text will be smaller on mobile. Using <code>:mobile { ...</code> shortcut.<ui-style>
<head> <script> Fez.styleMacro('mobile', '@media (max-width: 1200px)') </script> </head> <style> h1 { transition: font-size 0.3s ease; :mobile { font-size: 20px; } } </style> <div> <h1> <slot /> </h1> </div>
open in CodePen

Demo: ui-tabs

First tab


First tab

second tab

first tab

second tab


image tab

Third tab
  • Features: alternative node builder
  • nested and recursive components (tabs in tabs in tabs)
  • this.childNodes() FEZ instance helper function, get all first level child nodes, excluding #text nodes.
<ui-tabs> <div title="Bar"> <p>First tab</p> <br /> <ui-tabs> <div title="Foo nested 2">First tab</div> <div title="Bar"> <p>second tab</p> <ui-tabs> <div title="Foo nested 3">first tab</div> <div title="Bar nested 3"> <p>second tab</p> <hr /> <ui-clock></ui-clock> </div> </ui-tabs> </div> </ui-tabs> </div> <div title="Baz"> <h4>image tab</h4> <img src="./demo/fez.png" /> </div> <div title="Foo"> Third tab <hr /> <ui-clock></ui-clock> </div> </ui-tabs>
<script> activateNode(node) { // Remove active class from siblings Array.from(node.parentElement.children).forEach(child => { child.classList.remove('active') }) node.classList.add('active') } activate(num) { this.active = parseInt(num) const header = this.root.querySelector('div.header') const target = header.children[num] this.activateNode(target) this.activateNode(this.tabs[num]) } init(props) { this.root.style.width = 'calc(100%)' const { n, activate } = this; this.tabs = this.childNodes() this.render([ n('div.header', this.tabs.map((tab, index) => n('span', tab.getAttribute('title'), { onclick: ()=> activate(index) }) )), n('.body', '<slot />') ]); this.activate(0) } </script> <style> .fez-ui-tabs { body > div { display: none; } } :fez { --tabs-border: 1px solid #ccc; max-width: calc(100%); .header { margin-bottom: -2px; position: relative; z-index: 1; & > span { border: var(--tabs-border); padding: 8px 15px; display: inline-block; border-radius: 8px 8px 0 0; margin-right: -1px; background: #eee; cursor: pointer; &.active { background-color: #fff; border-bottom: none; } } } .body { border: var(--tabs-border); padding: 8px 15px; background: #fff; & > div { display: none; &.active { display: block; } } } } </style>
open in CodePen

Demo: ui-time

  • Slot state preservation demo:  
  • Features: slots - preserves original slot (fez tag innerHTML) on re-render
  • global and local css. If :fez { ... } is present, anything inside is local, anything outside is global. If no class :fez is present, style is considered local.
<ui-time city="Zagreb"> <ul> <li> Slot state preservation demo:   <b class="color-name"></b> </li> </ul> </ui-time>
<script> NAME = 'div' getRandomColor() { const colors = ['red', 'blue', 'green', 'teal', 'black', 'magenta', 'orange', 'lightblue'] return colors[Math.floor(Math.random() * colors.length)] } setRandomColor() { const color = this.getRandomColor() // this.find('.color-name').innerHTML = color this.root.querySelector('.color-name').innerHTML = color this.root.style.borderColor = color } getTime() { return (new Date()).getTime() } setTime() { this.val('.time', this.getTime()) } afterRender() { this.setTime() } init() { this.setRandomColor() } </script> <style> /* styles are applied to body, so this becomes body background color */ background-color: #f7f7f7; /* local component style applied to mounted component root */ :fez { border: 10px solid green; border-radius: 10px; padding: 10px; background-color: #fff; button { font-size: 16px; } } </style> <p>Param city: {{ props.city }}</p> <p>Time now: <span class="time"></span></p> <p>Random num: <span>{{ Math.random() }}</span></p> <button onclick="fez.setRandomColor()">random color</button> ⋅ <button onclick="fez.render()">refresh & preserve slot</button> <hr /> <slot></slot>
open in CodePen

Demo: ui-tippy

To enable, add add tooltip="..." to any node.

Basic Examples

Hover this link

Dynamic Content

Icons and Images

โš™๏ธ โ„น๏ธ โš ๏ธ
  • Features: Nothing!. Blank compoenent, only adds to head on load.
<p> To enable, add add <code>tooltip="..."</code> to any node. </p> <h4>Basic Examples</h4> <button tooltip="Hi from tippy.js">Hover me</button> <button tooltip="Another tooltip!">Another button</button> <a href="#" tooltip="Works on links too">Hover this link</a> <h4>Dynamic Content</h4> <button onclick="this.setAttribute('tooltip', 'Updated at ' + new Date().toLocaleTimeString())"> Click me then hover (dynamic tooltip) </button> <h4>Icons and Images</h4> <span tooltip="Settings" style="font-size: 24px; cursor: pointer;">โš™๏ธ</span> <span tooltip="Information" style="font-size: 24px; cursor: pointer;">โ„น๏ธ</span> <span tooltip="Warning!" style="font-size: 24px; cursor: pointer;">โš ๏ธ</span>
<head> <link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css"> <script type="module"> // add tooltip="..." to any node import tippy from 'https://cdn.skypack.dev/tippy.js' const bindTippy = () => { const ttName = 'tooltip' document.querySelectorAll(`[${ttName}]`).forEach(node => { tippy(node, { content: node.getAttribute(ttName), arrow: true }) node.removeAttribute(ttName) }) } bindTippy() setInterval(bindTippy, 1000) </script> </head>
open in CodePen

Demo: ui-toast


  • Features: class header that gets executed on component initializer.
<form onsubmit="showToast(this); return false"> <input type="text" name="toast" /> <button class="btn">Show info</button> </form> <br /> <button class="btn-sm" onclick="Toast.info('Hi')">Trigger info</button> <button class="btn-sm" onclick="Toast.error('Graaa')">Trigger error</button> <script> function showToast(form) { let input = form.querySelector('input') Toast.info(input.value) input.value = '' } </script>
<script> GLOBAL = 'Toast' error(text) { this.info(text, 'error') } info(text, klass) { const node = document.createElement('div') node.className = `toast ${klass || 'info'}` node.innerHTML = text this.find('.parent').prepend(node) setTimeout(()=>{ node.classList.add('leave') setTimeout(()=>node.remove(), 2000) }, 4000) } </script> <style> .parent { width: 300px; position: fixed; top: 20px; right: 20px; div.toast { padding: 10px 20px; background-color: #fff; margin-bottom: 20px; border-radius: 10px; box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; animation: dropIn 0.5s ease-out; &.leave { animation: dropOut 0.3s ease forwards; } &.info { border: 1px solid lch(50% 50 140);; background-color: lch(90% 50 140); } &.error { border: 1px solid lch(50% 50 350);; background-color: lch(90% 50 350); } } } @keyframes dropIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } @keyframes dropOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(20px); } } </style> <div class="parent"></div>
open in CodePen

Demo: ui-todo

  • fully reactive state
  • template for loop
  • Features: fez-bind - two way binding of input element
  • Features: fez-use - when node is added to dom, call described function and pass node as reference (inspired by Svelte)
<ui-todo></ui-todo>
<script> clearCompleted() { this.state.tasks = this.state.tasks.filter((t) => !t.done) } removeTask(index) { this.state.tasks = this.state.tasks.filter((_, i) => i !== index); } addTask() { // no need to force update template, this is automatic because we are using reactiveStore() this.counter ||= 0 this.state.tasks.push({ name: `new task ${++this.counter}`, done: false, animate: true }) } animate(node) { // same as in Svelte, uf you define fez-use="methodName", method will be called when node is added to dom. // in this case, we animate show new node node.style.display = 'block' node.style.transition = 'height 200ms, opacity 200ms' node.style.height = '33px' node.style.opacity = '1' setTimeout(() => { delete this.state.tasks[this.state.tasks.length-1].animate node.style.height = 'auto' node.style.transition = '' }, 200) } init() { this.state.tasks = [ {name: 'First task', done: false}, {name: 'Second task', done: false}, {name: 'Third task', done: true }, ] } </script> <h3>Tasks</h3> {{if !state.tasks[0] }} <p>No tasks found</p> {{/if}} {{for task, index in state.tasks}} {{if task.animate}} <!-- this is fine because this is string templating --> <p fez-use="animate" style="display: none; height: 0px; opacity: 0;"> {{:else}} <p> {{/if}} <input type="text" fez-bind="state.tasks[{{index}}].name" style="{{ task.done ? 'background-color: #ccc;' : '' }}" /> ⋅ <input type="checkbox" fez-bind="state.tasks[{{index}}].done" /> ⋅ <button onclick="fez.removeTask({{ index }})">×</button> </p> {{/for}} <p> <button onclick="fez.addTask()">add task</button> ⋅ <button onclick="fez.clearCompleted()">clear completed</button> </p> <pre class="code">{{ JSON.stringify(state.tasks, null, 2) }}</pre> <p>If you want to preserve state in templates, wrap content in "fez-slot"</p> <p>Refresh: {{Math.random()}}</p> <p class="fez-slot"> Do not refresh: {{Math.random()}}. </p>

Fez was created by @dux in 2024.