Clone on Gitea
Introduction
Explained by ducks
FAQ
Getting Started
Components
Component Basics
Auto Initialization From DOM
Templates
Children and Parents
Roles
Strictness
Fields
What is a Field
Binding Fields to DOM
Reading and Writing Fields
Value Casting
Flags
Events
Overview
Event Handlers
DOM Events
Custom Events
Propagation and Flow
Advanced Event Control
Beyond Components
States
Overview
StateEngine
DisplayStateEngine
Field Matchers
Ordering and priority
Debugging
Advanced usage
Ajax
ComponentUI
VirtualForm
Unit Testing
Helpers
Overview
Cookies
Arrays

Qite.js

Frontend framework for people who hate React and love HTML.

  • No build step, no Virtual DOM, no npm, no mixing JavaScript with HTML.
  • DOM-first, SSR-first, and fully usable with plain browser APIs.
  • Small, self-sufficient, and powerful enough for serious apps.

If you want your UI to feel like structured HTML enhanced with clear, declarative behavior instead of a compiled abstraction layer, Qite.js is for you.

Why Qite.js exists

Modern frontend frameworks made a simple idea complicated: take some HTML, react to user input, update the page.

Instead of treating the DOM as the source of truth, they introduced virtual DOMs, render cycles, hydration layers, build pipelines, custom syntaxes and a huge dependency ecosystem. In many stacks, simply rendering a button now comes with transpilation, bundling, component re-execution and many layers that are not the browser.

Qite.js goes the opposite way.

  • It treats the DOM as the source of truth and manipulates it directly.
  • It doesn't re-render from scratch and doesn't simulate the browser in memory first.
  • It works with plain HTML and plain JavaScript modules.
  • It doesn't require npm — Qite is one repository you can add as a git submodule or copy into your project.

If you have ever thought: "why is frontend so bloated now?", Qite.js may be for you.

No build step. No virtual DOM.

Qite.js runs directly in the browser. You include it on the page and use it.

<script src="/assets/vendor/qite/src/qiteinit.js"></script>
<script src="/assets/js/init.js" type="module"></script>

Then you write components in plain JavaScript. There is no JSX, no transpiler, no bundler requirement, and no special template language. HTML is never mixed with JavaScript — markup stays markup, behavior stays behavior, and styling stays styling. Qite also works naturally with standard CSS transitions and animations, whether they are your own or come from one of Qite's standard components.

SSR first, SPA when you want it

Qite.js fits naturally into server-side rendered applications. You render HTML on the server and Qite attaches behavior to it on the client.

That means you can:

  • Keep most pages fully SSR.
  • Make selected sections behave like small SPAs.
  • Build a full SPA if you really want to.

There is no forced architecture, because Qite enhances what is already there instead of making you rebuild your app around a framework's worldview.

DOM as the source of truth

Qite.js doesn't keep a shadow copy of the UI — each component is attached to a real DOM element:

<div data-component="Counter"></div>

The component manipulates that element directly. When something changes, the DOM is updated immediately: there is no diffing step or reconciliation pass and no synthetic re-render cycle. Qite's philosophy is that the browser is already very good at DOM work and instead of simulating the DOM first and touching the real DOM later, Qite works with the real thing from the start.

Declarative states

Showing, hiding and coordinating UI shouldn't require scattered if statements and repeated manual toggling and Qite gives you declarative state rules instead — you define rules that depend on fields and flags:

static display_states = [
    { active_mode: "all" },

    [ { status: "loading" }, "#spinner" ],
    [ { status: "error" },   "#error"   ],
    [ { status: "ready" },   ".content" ],

    [
        { status: ["loading", "error"] }, null,
        [
            [ { flags: ["debug"] }, "#debug_panel" ]
        ]
    ]
];

Then Qite reevaluates those rules whenever relevant fields or flags change and the result is predictable UI without class-toggling spaghetti.

Children are dumb, parents are smart

Components in Qite form a hierarchy: child components emit events, but they don't normally decide what the application should do. Instead, it's parents job to listen, interpret, and make decisions. A ButtonComponent doesn't need to know whether it submits a checkout form, retries a failed request, or opens a dialog — it only needs to do button things. Its the parent gives it meaning through roles and event handling.

Fields and flags are built in

Qite already gives you two kinds of component state:

  • fields for structured values such as status, email, price, tracking_number
  • flags for simple on/off state such as open, saving, locked, debug

Fields are mirrored to the DOM and can be bound declaratively to text or attributes. Flags are not mirrored to the DOM and are mainly used for behavior and state matching.

When fields or flags change:

  • the component state updates
  • bound DOM updates
  • state engines reevaluate automatically

Events are simple and explicit

Components communicate through events. A child component may publish:

this.events.publish("submit");

and its parent may listen to it by role:

this.events.add([
    ["submit", ">checkout_form", () => this.processCheckout()]
]);

The same API also handles DOM events:

// @click with an @ simply means it's a native DOM API event.
this.events.add([
    ["@click", "#retry", () => this.retry()]
]);

This gives Qite one event model for both browser events and custom component signals. There's no global bus, hidden magic or re-render-triggered side effects.

Templates instead of render functions

When you need to create components after page load, Qite uses native HTML <template> elements.

That means:

  • markup stays in HTML
  • behavior stays in JavaScript
  • dynamically created components behave exactly like initial ones

Qite doesn't ask you to build HTML strings or invent mini-programs inside JavaScript just to produce DOM nodes.

A realistic component example

Here is a made-up but realistic example of the kind of component you might often find yourself building with Qite. It shows a small shipping quote panel. The user enters destination and weight, then clicks a standard ButtonComponent child with role "lookup". The parent component:

  • reads and writes fields
  • uses a flag
  • handles DOM and child events
  • performs Ajax request
  • publishes its own custom event
  • uses both states and display_states
  • coordinates standard child components by role

Let's start with HTML first. Imagine this is rendered by your backend:

<section data-component="ShippingQuote"
    data-destination=""
    data-weight_kg="1"
    data-price=""
    data-currency="EUR"
    data-error=""
>
    <header>
        <h2>Shipping quote</h2>
        <p data-part="subtitle">Get an instant estimate.</p>
    </header>

    <div data-part="form">
        <label>
            Destination
            <input data-field-map="destination:value"
                type="text"
                value=""
                placeholder="Netherlands"
            />
        </label>

        <label>
            Weight (kg)
            <input data-field-map="weight_kg:value"
                type="number"
                min="0.1"
                step="0.1"
                value="1"
            />
        </label>

        <div data-part="actions">
            <button data-component="Button" data-roles="lookup">Get quote</button>
            <button data-component="Button" data-roles="retry" hidden>Retry</button>
        </div>
    </div>

    <div data-part="spinner" hidden>Loading quote...</div>
    <div data-part="error" hidden data-field="error"></div>

    <div data-part="result" hidden>
        Price:
        <strong data-field="price"></strong>
        <span data-field="currency"></span>
    </div>
</section>

And now the component code:

import BaseComponent from "/assets/vendor/qite/src/components/base_component.js";
import Ajax from "/assets/vendor/qite/src/lib/ajax.js";

export default class ShippingQuoteComponent extends BaseComponent {

    static flags = [["loading", false]];

    static states = [
        [
            { flags: ["loading"] },
            {
                in:  (c) => c.ui.disable(">lookup"),
                out: (c) => c.ui.enable(">lookup")
            },
            { name: "loading" }
        ],

        [
            { fields: { price: "isPresent()" } },
            (c) => c.events.publish("quote_ready", {
                data: {
                    destination: c.fields.get("destination"),
                    weight_kg:   c.fields.get("weight_kg"),
                    price:       c.fields.get("price"),
                    currency:    c.fields.get("currency")
                }
            }),
            { name: "quote_ready" }
        ]
    ];

    static display_states = [
        { visibility_mode: "whitelist", active_mode: "all" },

        [{ flags: ["loading"]                     }, ["#spinner"]],
        [{ error: "isPresent()"                   }, ["#error", ">retry"]],
        [{ price: "isPresent()"                   }, ["#result"]],
        [{ price: "isBlank()", error: "isBlank()" }, ["#form", ">lookup"]],
    ];

    constructor(tree_node) {
        super(tree_node);

        this.events.add([
            ["@input", "self",   () => this.updateFieldsFromDOM()],
            ["@click", "lookup", () => this.fetchQuote()],
            ["@click", "retry",  () => this.fetchQuote()],
        ]);
    }

    async fetchQuote() {
        this.fields.set({ error: null, price: null });
        this.flags.set("loading", true);

        let resp = await Ajax.get("/shipping/quote", {
            destination: this.fields.get("destination"),
            weight_kg:   this.fields.get("weight_kg")
        }, {
            response_type: "json"
        }).ready();

        if (resp.ok) {
            this.fields.set({
                price: resp.data.price,
                currency: resp.data.currency || "EUR"
            });
        } else {
            this.fields.set({ error: "Could not fetch quote." });
        }

        this.flags.set("loading", false);
    }

}

// This is how components are added to Qite registry -- otherwise
// ShippingQuoteComponent instances won't be able to attach
// to corresponding HTML elements.
Qite.components.ShippingQuote = ShippingQuoteComponent;

This example is intentionally a bit larger than a toy snippet because it shows how Qite is usually meant to be used in practice:

  • HTML defines the structure and initial values
  • JavaScript attaches behavior
  • child components are found by role
  • fields hold structured state
  • flags drive temporary UI logic
  • Ajax stays explicit
  • states decide behavior and visibility declaratively

This is the core Qite mental model.

What Qite.js is and is not

Qite.js is

  • a DOM-first component framework
  • a declarative state system
  • a hierarchy-driven event system
  • an SSR-friendly frontend layer
  • a zero-build-step solution

Qite.js is not

  • a virtual DOM framework
  • a template compiler
  • a React clone
  • a mandatory SPA architecture
  • a framework that needs npm and a bundler to be usable