The Unified UI Language
One file. HTML + CSS + JS. Zero config.
HJX is a compiled UI language that unifies structure, style, and logic into a single .hjx file. It compiles to clean, dependency-free HTML + CSS + JavaScript β no virtual DOM, no runtime overhead, no framework lock-in.
component Counter
state:
count = 0
layout:
view#root.card:
text.title: "Count: {{count}}"
button.primary (on click -> inc): "Increase"
style:
.card { padding: 16px; border: 1px solid #ddd; border-radius: 12px; }
.primary { padding: 10px 14px; border-radius: 10px; cursor: pointer; }
handlers:
inc:
set count = count + 1
That's it. One file β a fully working interactive counter. No imports, no boilerplate, no configuration.
component Counter
state:
count = 0
layout:
view#root.card:
text.title: "Count: {{count}}"
button.primary (on click -> inc): "Increase"
button.ghost (on click -> dec): "Decrease"
style:
.card { padding: 16px; border: 1px solid #ddd; border-radius: 12px; display: inline-flex; flex-direction: column; gap: 12px; }
.primary { padding: 10px 14px; border-radius: 10px; cursor: pointer; border: 0; }
.ghost { padding: 10px 14px; border-radius: 10px; cursor: pointer; border: 1px solid #ddd; background: transparent; }
handlers:
inc:
set count = count + 1
dec:
set count = count - 1
component SimpleForm
state:
email = ""
msg = "Type your email"
layout:
view.card:
text.title: "Newsletter"
input.field (bind value <-> email)
text.hint: "You typed: {{email}}"
button.primary (on click -> submit): "Submit"
text.note: "{{msg}}"
handlers:
submit:
set msg = "Submitted β
"
component TodoList
state:
items = ["Learn HJX", "Build a UI", "Deploy to production"]
newItem = ""
showCompleted = false
layout:
view.container:
view.header:
text.title: "My Todo List"
text.count: "Items: {{items.length}} tasks"
view.input-section:
input.todo-input (bind value <-> newItem):
button.add-btn (on click -> addItem): "Add"
view.list:
for (todo in items):
view.todo-item:
text: "β’ {{todo}}"
view.footer:
button.toggle-btn (on click -> toggleCompleted): "Show Completed"
if (showCompleted):
text.completed-note: "π All tasks completed!"
handlers:
addItem:
set items = [...items, newItem]
set newItem = ""
toggleCompleted:
set showCompleted = !showCompleted
component Dashboard
imports:
Card from "./components/Card.hjx"
Button from "./components/Button.hjx"
state:
uptime = 0
serverTime = ""
cpuUsage = 45
status = "Operational"
alerts = ["High CPU Usage", "New login from unknown device"]
script:
export function init(store) {
setInterval(() => {
store.set({
uptime: store.get("uptime") + 1,
serverTime: new Date().toLocaleTimeString(),
cpuUsage: Math.floor(Math.random() * 20) + 30
});
}, 1000);
}
layout:
view.min-h-screen.bg-slate-50.p-8:
view.max-w-6xl.mx-auto.space-y-8:
view.text-3xl.font-bold: "System Dashboard"
view.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-4.gap-4:
Card (title="Uptime"):
view.text-2xl.font-bold: "{{uptime}}s"
Card (title="CPU"):
view.text-2xl.font-bold: "{{cpuUsage}}%"
for (alert in alerts):
view.bg-white.border.p-3.rounded.shadow-sm: "{{alert}}"
component CompositionDemo
imports:
Button from "./components/Button.hjx"
Input from "./components/Input.hjx"
Card from "./components/Card.hjx"
state:
name = ""
count = 0
layout:
view.min-h-screen.bg-background.flex.items-center.justify-center.p-4:
Card (class="w-[400px]" title="Login" description="Enter your details below"):
view.flex.flex-col.gap-4:
Input (placeholder="johndoe" bind value <-> name)
view.flex.gap-2:
Button (variant="outline" on click -> dec): "-"
Button (on click -> inc): "+"
Button (class="w-full" on click -> submit): "Submit"
handlers:
inc:
set count = count + 1
dec:
set count = count - 1
submit:
set message = "Hello " + name + "!"
- Node.js v18+
- npm v9+
# 1. Clone the repository
git clone https://github.com/loayabdalslam/hjx.git
cd hjx
# 2. Install dependencies
npm install
# 3. Build the compiler
npm run build
# 4. Build an example
node dist/cli.js build examples/counter.hjx --out dist-app
# 5. Start the dev server (with hot reload)
node dist/cli.js dev examples/counter.hjx --out dist-app --port 5173Open http://localhost:5173 and you're live.
node dist/cli.js dev examples/dashboard.hjx --out dist-app --port 5173This enables real-time server-side state management via WebSocket β state lives on the server, UI updates are pushed to the client automatically.
Every .hjx file follows this block structure:
component <Name> β Component declaration
imports: β Optional: import other .hjx components
state: β Reactive variables
script: β Optional: server-side initialization
layout: β UI tree (indentation-based)
style: β Scoped CSS
handlers: β Event logic
Declares the component name. One component per file.
component MyApp
Defines reactive, component-local variables. Supports numbers, strings, booleans, arrays, and objects.
state:
count = 0
title = "Hello"
enabled = true
items = ["todo1", "todo2"]
user = { name: "John", age: 30 }
Defines the UI tree using indentation. Think of it as a cleaner, whitespace-sensitive HTML.
| Syntax | Description |
|---|---|
view |
Generic container (<div>) |
text |
Inline text (<span>) |
button |
Button element |
input |
Input element |
view#id.class1.class2: |
ID + classes |
text: "Hello {{name}}" |
Text interpolation |
button (on click -> handler): "Label" |
Event binding |
input (bind value <-> stateVar) |
Two-way binding |
layout:
if (isLoggedIn):
text: "Welcome back!"
if (!isLoggedIn):
text: "Please log in."
if (status === "active"):
text: "Account is active"
for (item in items):
view.row:
text: "{{item}}"
Supported operators: ! (negation), === (equality), != (inequality)
Raw CSS, automatically scoped to the component via [data-hjx-scope] attribute selectors.
style:
.card { padding: 16px; border-radius: 12px; }
.primary { background: #007bff; color: white; }
Supports Tailwind-style class names with : and / characters.
Defines event handler logic using a simple statement language.
handlers:
increment:
set count = count + 1
reset:
set count = 0
log "Counter reset"
Statements: set <var> = <expr>, log "<message>"
Expressions: numbers, identifiers, + - * /, parentheses, state variables
Import and compose other .hjx components:
imports:
Button from "./components/Button.hjx"
Card from "./components/Card.hjx"
Use them in layout with props and slots:
layout:
Card (title="My Card"):
text: "This is slot content"
Button (variant="primary" on click -> save): "Save"
Run initialization logic server-side. The init(store) function is called with a reactive store:
script:
export function init(store) {
setInterval(() => {
store.set({ timestamp: Date.now() });
}, 1000);
}
| Command | Description |
|---|---|
hjx parse <file.hjx> |
Print the AST (JSON) for a file |
hjx build <file.hjx> --out <dir> |
Compile to index.html + app.css + app.js |
hjx dev <file.hjx> --out <dir> --port <n> |
Build, serve, and watch with hot reload |
Vanilla target emits:
index.htmlβ Minimal page with scoped stylesapp.cssβ Scoped component stylesapp.jsβ Runtime + compiled component logic
Server-driven target adds:
- WebSocket synchronization
- Server-managed state evaluation
- Real-time push updates
HJX ships with a first-party Vite plugin for seamless integration into modern build pipelines:
# In your Vite project:
npm install vite-plugin-hjx --save-dev// vite.config.js
import { defineConfig } from 'vite';
import hjxPlugin from 'vite-plugin-hjx';
export default defineConfig({
plugins: [hjxPlugin()]
});Then import .hjx files directly:
import App from './App.hjx';
App.mount(document.getElementById('app'));Features: HMR support, CSS injection, automatic scoping.
Benchmarked on Windows x64 β’ Node.js β’ JSDOM environment
Date: 2026-02-17
| Workload | Time |
|---|---|
| Parse 100 state variables | 2.15 ms |
| Parse 1,000 state variables | 1.90 ms |
| Parse 5,000 state variables | 11.25 ms |
| Parse 100 static nodes | 0.95 ms |
| Parse 1,000 static nodes | 2.55 ms |
| Parse 5,000 static nodes | 17.12 ms |
| Workload | Time |
|---|---|
| Compile 100 nodes β Vanilla JS | 1.73 ms |
| Compile 1,000 nodes β Vanilla JS | 2.87 ms |
| Compile 5,000 nodes β Vanilla JS | 13.19 ms |
| Scope 100 CSS rules | 0.33 ms |
| Scope 1,000 CSS rules | 1.86 ms |
| Workload | Render | Update |
|---|---|---|
| Static 100 items | 33 ms | β |
| Static 1,000 items | 135 ms | β |
| List 100 items | 59 ms | 27 ms |
| List 1,000 items | 286 ms | 217 ms |
| Conditional 100 items | 142 ms | 2 ms |
| Conditional 1,000 items | 8,057 ms | 14 ms |
| Text interpolation 100 items | 46 ms | 2 ms |
| Input binding 100 items | 63 ms | 1 ms |
Key insight: Updates are extremely fast (sub-3ms for 100 items) thanks to targeted DOM patching. Initial render scales linearly.
| Workload | Time |
|---|---|
| Init session (100 handlers) | 8.59 ms |
| Init session (1,000 handlers) | 9.82 ms |
| Execute 1,000 handler calls | 3,548 ms (3.5 ms/call) |
hjx/
βββ src/ # Compiler source (TypeScript)
β βββ parser.ts # HJX β AST
β βββ compiler/ # AST β HTML/CSS/JS
β β βββ vanilla.ts # Vanilla JS target
β βββ runtime.ts # Client-side reactivity
β βββ server_session.ts # Server-driven state manager
β βββ devserver.ts # Dev server with HMR
β βββ cli.ts # CLI entry point
βββ examples/ # Example .hjx files
β βββ counter.hjx
β βββ form.hjx
β βββ list.hjx
β βββ conditional.hjx
β βββ dashboard.hjx
β βββ composition_demo.hjx
β βββ components/ # Reusable components
β βββ Button.hjx
β βββ Card.hjx
β βββ Input.hjx
βββ packages/
β βββ vite-plugin-hjx/ # First-party Vite plugin
βββ extensions/
β βββ vscode/ # VS Code extension
βββ dist/ # Compiled output
βββ package.json
PRs are welcome! If you'd like to contribute:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m 'Add my feature') - Push to the branch (
git push origin feature/my-feature) - Open a Pull Request
MIT Β© Loay Abdalslam