<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

examplesplaygroundGitHub repo

<html> <head> <title>Time</title> <script script="/fez/core.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.

With reactive state and custom style.

  1. Add Fez JS (to HEAD)
  2. Create ex-counter component

    Or load via HTTP (as in this case, view source)

    <template fez="/demo/ex-counter.fez.html"></template>
  3. Place ex-counter anywhere in BODY
  4. That is it! Edit this demo on JSbin

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: ui-avatar

  • features blocks {{#block ...}}
<style> .avatars { img.avatar { border: 2px solid #ddd; background-color: #fff; margin-right: -10px; } } </style> <div class="avatars"> <ui-avatar src="https://robohash.org/a.png" name="Dux"></ui-avatar> <ui-avatar src="https://robohash.org/b.png" name="Mile"></ui-avatar> <ui-avatar src="https://robohash.org/c.png?set=set2"></ui-avatar> <ui-avatar src="https://robohash.org/d.png?set=set2"></ui-avatar> <ui-avatar src="https://robohash.org/e.png" name="Joza"></ui-avatar> </div>
<template fez="ui-avatar"> <script> NAME = 'span' FAST = true connect(props) { this.copy('href', 'style') this.size = (props.size || 64) + 'px' this.root.style.width = this.size this.root.style.height = this.size } image() { return `<img src="${this.props.src}" class="avatar" style="width: ${this.size}; height: ${this.size};" />` } </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> {{#if @props.name}} <span class="avatar"> <div class="title">{{ @props.name }}</div> <div> {{@html @image() }} </div> </span> {{:else}} {{@html @image() }} {{/if}} </template>
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>
<template fez="ui-border"> <script> onMount() { const childNodes = this.fezHide() childNodes.forEach(node => { if (node.nodeType === 1) { // Element node node.style.border = `3px solid ${this.props.color || 'black'}` } }) } </script> </template>
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)
<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> <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>
<template fez="ui-card"> <script> flip() { this.find('.card').classList.toggle('flipped') } autoflip() { setTimeout(()=>{ this.flip() this.autoflip() }, 5000 + (Math.random() * 15* 1000)) } connect() { 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="@flip()"> <slot /> </div> </template>
open in CodePen

Demo: ui-clock

  • SVG generation
  • Features: reactive store - any update to this.state object triggers re-render.
  • Features: DOM morph updates - update only changed nodes and attributes
  • Features: dynamic styes (seconds line color)
  • 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>
<template fez="ui-clock"> <script> randomColor() { const colors = ['red', 'green', 'blue', 'magenta', 'teal'] 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.state.color = this.randomColor() } connect() { 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" style="--color: {{ @state.color }}"> <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> </template>
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>
<template fez="ui-editor"> <script> // you need to escape script tag inside fez script block Fez.head('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs2015.min.css') Fez.head('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/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.'); } } connect(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()) Fez.untilTrue(()=>{ // hljs has a bug on applying styles on first load, this fixes it if (window.hljs) { if (!node.classList.contains('hljs')) { hljs.highlightElement(this.codeNode) } else { return true } } }) } } </script> <style> code { padding: 10px; font-size: 15px; } pre { margin-top: 10px; width: 100%; line-height: 26px; } </style> {{#if @props.file}} <div style="font-size: 14px; margin: 0; position: relative; top: 7px;"> {{ @props.file }} ⋅ <button onclick="@copy()">Copy</button> {{#if @props.action }} ⋅ <button style="cursor: pointer; font-weight: 600;" onclick="@props.action('{{ @props.name }}')" >Update</button> {{/if}} </div> {{/if}} <pre> <code class="language-{{ @props.language }}" contenteditable="{{ !!@props.file }}"></code> </pre> </template>
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> <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>
<template fez="ui-form"> <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> </template>
open in CodePen

Demo: ui-icon



delete
  • Features: component to component communication
  • evaluate value of a attribute if prefixed with column : (in this example :size=
<input type="range" min="24" max="100" class="slider" id="icon-range" oninput="Fez('#icon-blue').setSize(this.value)" /> ⋅ <span onclick="if (event.target.tagName === 'BUTTON') Fez('#icon-blue').attr('color', event.target.textContent)"> <button>red</button> <button>blue</button> <button>green</button> </span> <br /><br /> <ui-icon name="home"></ui-icon> <ui-icon id="icon-blue" name="settings" color="blue" onclick="alert(this.fez.props.color)" :size="document.getElementById('icon-range').value" ></ui-icon> <ui-icon color="red">delete</ui-icon>
<template fez="ui-icon"> <script> // everything before class gets executed in component registration if (!document.querySelector('link[href*="Material+Symbols+Outlined"]')) { const link = document.createElement('link') link.rel = 'stylesheet' link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200' document.head.appendChild(link) } class { NAME = 'span' setSize(size) { this.root.style.fontSize = `${parseInt(size)}px` } onPropsChange(name, value) { if (name == 'color') { this.root.style.color = value } if (name == 'size') { this.setSize(value) } } connect(props) { this.copy('onclick') const icon = props.name || this.root.innerHTML.trim() this.color = props.color || '#00' this.root.classList.add('material-symbols-outlined') this.root.innerHTML = icon } } </script> <style> &.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24 } </style> </template>
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: Fez.getScript(src, attr, callback) that will insert script css in head
  • Features: Fez.untilTrue() to initialize component when all files are loaded.
<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>
<xmp fez="ui-markdown"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.1.3/marked.min.js"></script> </head> <script> connect(props) { let text = props.html || this.root.innerHTML; this.root.classList.add('markdown'); Fez.untilTrue(() => { if (window.marked) { text = window.marked(text) this.root.innerHTML = text; return true } }); } </script> </xmp>
<head> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.1.3/marked.min.js"></script> </head> <script> connect(props) { let text = props.html || this.root.innerHTML; this.root.classList.add('markdown'); Fez.untilTrue(() => { if (window.marked) { text = window.marked(text) this.root.innerHTML = text; return true } }); } </script>
open in CodePen

Demo: ui-pub-sub

  • Features: publish <> subscribe mechanism. Auto disconnect on node destroy.
  • Direct Fez component creation via Fez(tagName, class { ... })
⋅ Fast ping:

listener 1

listener 2

listener 3

Foo:

<div class="info"> <ul> <li>Features: publish <> subscribe mechanism. Auto disconnect on node destroy.</li> <li>Direct Fez component creation via <code>Fez(tagName, class { ... })</code></li> </ul> </div> <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 } connect() { this.target = this.find('.target') this.subscribe('ping', this.update) this.update('waiting for a ping...') } }) setInterval(()=>Fez.publish('ping', Math.random()), 5000) </script>
<template fez="ui-pub-sub"> <--! defined in HTML block --> </template>
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>
<template fez="ui-slider"> <script> cssVars() { return [ `--arrow-width: ${this.arrowWidth}px`, `--offset: -${this.state.offset}px`, ] } setSlide(num) { this.state.slide = num } changeSlide(direction){ this.state.slide += direction const slides = this.find('.fez-slot').querySelectorAll(":scope > div") if (this.state.slide < 0) { this.state.slide = slides.length - 1 } else if (!slides[this.state.slide]) { this.state.slide = 0 } } beforeRender() { const node = this.find('.slot') if (node) { this.state.offset = node.getBoundingClientRect().width * this.state.slide } } connect() { this.arrowWidth = 70 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" style="{{ @cssVars().join(';') }}"> <tr> <td class="arrow" onclick="@changeSlide(-1)"> <div> <span>⇧</span> </div> </td> <td> <div class="slot"> <div class="slot-parent"> <slot /> </div> </div> </td> <td class="arrow" onclick="@changeSlide(1)"> <div> <span>⇧</span> </div> </td> </tr> </table> </template>
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>
<template fez="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]) } connect(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> --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> </template>
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>
<template fez="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() } connect() { 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="@setRandomColor()">random color</button> ⋅ <button onclick="@render()">refresh & preserve slot</button> <hr /> <slot></slot> </template>
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 onclick="Toast.info('Hi')">Trigger info</button> <button onclick="Toast.error('Graaa')">Trigger error</button> <script> function showToast(form) { let input = form.querySelector('input') Toast.info(input.value) input.value = '' } </script>
<template fez="ui-toast"> <script> // helper function window.Toast = { info: (text, klass) => { Fez('#ui-toast').info(text, klass) }, error: (text) => { Toast.info(text, 'error') } } // auto init document.body.insertAdjacentHTML('beforeend', '<ui-toast id="ui-toast"></ui-toast>') class { 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> </template>
open in CodePen

Demo: ui-todo

  • fully reactive state
  • 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>
<template fez="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) } connect() { 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="@removeTask({{ index }})">×</button> </p> {{/for}} <p> <button onclick="@addTask()">add task</button> ⋅ <button onclick="@clearCompleted()">clear completed</button> </p> <pre class="code">{{ JSON.stringify(this.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> </template>

Fez was created by @dux in 2024.