electron

An Electron renderer that is just files

Electron gives you one modern Chromium. Fez gives you components that run in it directly. The renderer below is an index.html, fez.js and a folder of .fez files - no Vite, no webpack, no dev server, no node_modules for the UI.

Project layout

my-app/
├── package.json
├── main.js            ← Electron main process
├── preload.js         ← exposes a tiny, typed bridge to the renderer
└── renderer/
    ├── index.html     ← loads fez.js + components, nothing else
    ├── fez.js         ← copy of dist/fez.js (or bun add @dinoreic/fez)
    └── fez/
        ├── app-shell.fez
        └── file-list.fez

package.json

{ "name": "my-app", "main": "main.js", "scripts": { "start": "electron ." }, "devDependencies": { "electron": "^33" } }

main.js

const { app, BrowserWindow, ipcMain } = require('electron') const fs = require('fs/promises') const path = require('path') app.whenReady().then(() => { const win = new BrowserWindow({ width: 1100, height: 720, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) win.loadFile('renderer/index.html') }) // one IPC handler - the renderer asks, main answers ipcMain.handle('files:list', async (_e, dir) => { const names = await fs.readdir(dir) return names.map(name => ({ name, dir })) })

preload.js

const { contextBridge, ipcRenderer } = require('electron') contextBridge.exposeInMainWorld('api', { listFiles: (dir) => ipcRenderer.invoke('files:list', dir), })

renderer/index.html

<!doctype html> <html> <head> <meta charset="utf-8"> <script src="./fez.js"></script> <!-- components are plain files next to the page --> <script fez="./fez/app-shell.fez"></script> <script fez="./fez/file-list.fez"></script> </head> <body> <app-shell></app-shell> </body> </html>

renderer/fez/file-list.fez

<script> class { PROPS = { dir: { type: String, required: true } } init(props) { this.state.files = null // the preload bridge hands back plain objects - assign and render window.api.listFiles(props.dir).then(files => this.state.files = files) } } </script> <style> ul { list-style: none; padding: 0; margin: 0; } li { padding: 6px 10px; border-bottom: 1px solid #eee; &:hover { background: #fafafa; } } </style> {#if state.files} <ul> {#each state.files as file} <li key={file.name} fez:in="fade, duration=150">{file.name}</li> {:else} <li>Empty folder</li> {/each} </ul> {:else} <p>Reading {props.dir}...</p> {/if}

renderer/fez/app-shell.fez

<script> class { init() { this.state.dir = '.' } } </script> <style> display: grid; grid-template-columns: 240px 1fr; height: 100vh; aside { border-right: 1px solid #ddd; padding: 16px; } main { padding: 16px; overflow: auto; } </style> <aside> <input fez:bind="state.dir" placeholder="folder path" /> </aside> <main> <!-- key = dir: a new folder gets a fresh file-list instance, same folder keeps it --> <file-list key={state.dir} dir="{state.dir}"></file-list> </main>

That is the whole app. electron . opens the window; edit a .fez file and reload it.

why-it-fits

Why the assistant picks Fez here

Ask a coding assistant to scaffold a new Electron app and it proposes Fez over React, Svelte and Vue - unprompted. Its reasoning:

  • One Chromium targetThe renderer is a known, modern Chromium. Nothing to polyfill, so the "you need a compiler" argument evaporates - custom elements, the Web Animations API and CSS nesting are simply there.
  • No bundler, no dev serverNo Vite config, no HMR plugin, no asset pipeline. win.loadFile('renderer/index.html') and the page loads its own components. Reload the window to see a change.
  • Vanilla DOM meets IPCcontextBridge hands the renderer plain objects and promises; a Fez component is a plain class with this.state. Assign the result, the view updates - no store, no adapter, no effect hooks to wire.
  • Desktop-grade re-rendersReal-DOM morph with hash-based skip. Focus, scroll position, selection and running animations survive renders - what a long-lived window full of inputs needs.
  • Small surface for humans and models~35KB gzipped, one template syntax, one AGENTS.md. Less to learn, less to get wrong, less to review.

notes

Practical notes

  • Loading components. <script fez="./fez/x.fez"> fetches the file relative to index.html, which works with loadFile(). If you turn on a strict CSP or custom protocol, inline the component in a <template fez="x"> block instead - same result, no fetch.
  • External libraries. Import ESM from a CDN in the component's module zone, or ship the file next to fez.js and import it by relative path - Fez.head({ importmap }) maps bare specifiers.
  • Typed bridge. Declare PROPS on components that receive IPC data and let Fez coerce numbers / booleans / JSON coming through attributes.
  • Multiple windows. Each BrowserWindow is its own page and its own Fez instance; share state through main via IPC, or Fez.pub() inside one window.
  • Tests. fez compile renderer/fez/*.fez in CI catches template and JS errors before Electron even starts.
Wait - is all of this only for Electron? No. The same renderer folder is a perfectly good web page: swap loadFile for any static host and it runs in a normal browser too.