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
statesanddisplay_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