# el-ui Framework Specification
Version 1.2.0 — May 4, 2026
---
## Overview
el-ui is a frontend framework where state is an Engram graph and reactivity is spreading activation. Every framework must answer the same question: what re-renders when state changes? el-ui's answer is: whatever the spreading activation algorithm determines is relevant.
| Framework | Reactivity model |
|-----------|-----------------|
| React | Virtual DOM diffing |
| Vue | Dependency tracking (Proxy-based reactive primitives) |
| Svelte | Compile-time static analysis |
| Solid | Fine-grained signal-based subscriptions |
| **el-ui** | **Spreading activation over a typed state graph** |
The core insight: component state is represented as a graph of related nodes, and the same spreading activation algorithm used by the Engram knowledge engine determines which components need to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks.
---
## 1. Activation-Based Reactivity Model
### 1.1 The Model
Every piece of state in an el-ui application is a **node** in an in-browser Engram graph. Nodes have:
| Field | Type | Description |
|-------|------|-------------|
| `id` | String (UUID) | Node's permanent identity |
| `type` | String | `'state'`, `'route'`, or user-defined |
| `name` | String | State variable name |
| `content` | Any | Current value |
| `importance` | Float [0, 1] | Salience weight; boosted on update |
Nodes are connected by directed **edges** with:
| Field | Type | Description |
|-------|------|-------------|
| `weight` | Float [0, 1] | Connection strength |
| `relation` | String | Semantic label of the relationship |
### 1.2 Activation Formula
When state changes via `setState()`, spreading activation runs from the changed node through the graph:
```
strength = parent_strength × edge.weight × target.importance
```
This formula is identical to the Engram core spreading activation formula, with the semantic similarity factor (cosine_sim) omitted because the in-browser graph does not yet carry embedding vectors. In a future version connected to the Engram server, the full four-factor formula applies:
```
strength = parent_strength × edge.weight × target.importance × cosine_sim(query, target)
```
### 1.3 Why Multiplication, Not Addition
Addition allows many weak signals to accumulate into false relevance. The brain's associative memory is conjunctive: an activated path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero — weak edge, dormant node, or irrelevant connection — the path dies immediately. This is not a performance optimization; it is a semantic property of associative retrieval.
### 1.4 Pruning Threshold
Paths with activation strength below `PRUNE_THRESHOLD` (default: `0.01`) are cut. This prevents exponential blowup in large state graphs and models the brain's attention filter. All nodes not reached above the threshold are not considered for re-render.
### 1.5 Importance Boost (Long-Term Potentiation Analog)
When a state node is updated, its `importance` increases by 0.1 (capped at 1.0):
```
node.importance = Math.min(1.0, node.importance + 0.1)
```
Recently-changed state becomes more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance. State that changes often gains importance; state that never changes fades toward background salience.
### 1.6 Activation Surface
The **activation surface** is the set of nodes reached during a spreading activation pass, i.e., all nodes with `strength > PRUNE_THRESHOLD`. Components subscribed to any node in the activation surface receive update notifications and re-render.
---
## 2. Component Definition Syntax
Components are defined in `.el` files (shared extension with the El programming language). A component is a specialized el module with four optional sections.
### 2.1 Component Structure
```
component ComponentName {
props {
// prop declarations
}
state {
// state declarations
}
fn methodName(param: Type) -> ReturnType {
// method body
}
template {
// HTML template
}
}
```
All four sections are optional. Components with no template render an empty string. Multiple `fn` blocks are allowed; multiple `props` or `state` blocks are not (only the last one is used).
### 2.2 Props
Props are read-only inputs from the parent component.
```
props {
label: String // required prop
variant: String = "primary" // optional with default
disabled: Bool = false // boolean with default
onClick: Fn() -> Void // function prop
}
```
Supported prop types: `String`, `Int`, `Float`, `Bool`, `Fn(...) -> T`.
**Note:** Array (`[T]`) and optional (`T?`) type syntax is not currently parsed by the compiler. The parser reads a single identifier as the type name.
Props are accessed in generated code as `this._props_{name}` (not `this.props.name`). In method and template bodies, the compiler exposes each prop as a local constant named `{name}`.
### 2.3 State
State declarations create nodes in the component's Engram graph. Every state variable requires an initial value.
```
state {
count: Int = 0
label: String = "hello"
active: Bool = false
}
```
Each state variable becomes:
1. A node in `this._graph` (seeded at construction time with `type: 'state'`), tracked by ID in `this._stateNodes`.
2. A mirrored value in `this._state` for direct read access.
3. A reactive binding: changing the value via `setState()` triggers activation and re-render.
In method and template bodies, the compiler exposes each state variable as a local constant named `{name}` (reading from `this._state`).
### 2.4 Methods
Methods are `fn` definitions inside the component body. State assignments in method bodies compile to `setState()` calls.
```
fn increment() -> Void {
count = count + 1 // compiles to: __self.setState('count', count + 1)
}
```
The generated JavaScript method exposes all state variables as local `const` bindings before executing the body. The variable `__self` refers to the component instance.
---
## 3. Template Syntax
### 3.1 Interpolation
Any expression embedded in `{expr}`:
```
{count}
{"Hello, " + name + "!"}
{active ? "on" : "off"}
```
### 3.2 Event Binding
DOM events bound with `on:event={handler}`:
```
value = e.target.value} />
hovered = true} on:mouseleave={() => hovered = false} />
```
Supported events: `click`, `input`, `change`, `mouseenter`, `mouseleave`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `submit`, `mousedown`, `mouseup`, `dblclick`, `contextmenu`.
The compiler emits `data-el-{event}` attributes on elements. The renderer binds handlers at mount time using `new Function(...)` evaluated in the component's scope (`__self`), and rebinds after each patch.
### 3.3 Dynamic Attributes
```
```
### 3.4 Static Attributes
```
```
### 3.5 Boolean Attributes
The parser recognises a fixed set of boolean attribute names: `disabled`, `checked`, `readonly`, `required`, `multiple`, `selected`. These emit the attribute name when the expression is truthy, nothing when falsy.
```
```
A standalone attribute with no value (e.g., `
` |
| `RawText(s)` | Inline expression text, text content |
Non-template code tokens include keywords (`component`, `props`, `state`, `fn`, `template`, `if`, `else`, `return`), identifiers, literals (`String`, `Int`, `Float`, `Bool`), operators, and punctuation.
### 7.3 Parser
Hand-written recursive descent parser. Produces `Vec`. Each component is parsed as:
1. `component Name {` — component declaration
2. `props { ... }` — prop definitions (optional)
3. `state { ... }` — state definitions (optional)
4. `fn method(...) -> Type { ... }` — methods (optional, multiple allowed)
5. `template { ... }` — template tree (optional)
The template parses as a tree of `TemplateNode` values:
| Variant | Description |
|---------|-------------|
| `Element { tag, attrs, children }` | HTML element with attributes and children |
| `Component { name, props }` | Uppercase-initial component reference |
| `Text(s)` | Literal text content |
| `Interpolation(expr)` | `{expr}` — expression interpolation |
| `If { condition, then, else_ }` | `{#if}...{:else}...{/if}` |
| `Each { items, item_name, children }` | `{#each items as item}...{/each}` |
| `Activate { query, result_name, children }` | `{#activate "query" as name}...{/activate}` |
Method bodies are captured as raw source text and passed through to the code generator with simple string transformations.
**Unknown `{#blockname}` tags** produce a parse error (`unknown block tag: #name`).
### 7.4 Code Generator — Multi-Target
The code generator (`src/codegen.rs`) supports three compilation targets:
#### `CodegenTarget::Web` (default) — Legacy JavaScript
Emits an ES2022 JavaScript module using the el-ui JS runtime (`graph.js`, `renderer.js`, `router.js`, `index.js`). This is the primary target for browser-based applications today.
For each component:
1. Emits a class extending `Component` from the runtime.
2. Constructor: seeds state nodes into `this._graph`, sets prop values as `this._props_{name}`, subscribes to activation events.
3. Emits `setState(name, value)` which calls `this._graph.update()` to trigger activation.
4. Emits `render()` returning a template literal string.
5. Translates state assignments: `count = count + 1` → `__self.setState('count', count + 1)`.
6. Translates template interpolations: `{count}` → `${count }`.
7. Emits event handlers as `data-el-{event}` attributes for the renderer to bind.
8. Emits `${__self._child(ComponentClass, props)}` for component references.
#### `CodegenTarget::Server` — Node.js SSR
Emits a Node.js CommonJS module (the "render script") that exports `renderToString(componentName, props)` and `renderToDocument(componentName, props, shellOpts)`. The render script uses `runtime/src/ssr.js` to parse the component source at module load time and render to HTML strings at call time.
The `build_node_tree()` / template-to-HTML pipeline is implemented in `runtime/src/ssr.js`. It covers all TemplateNode variants: `Element`, `Text`, `Interpolation`, `If`, `Each`, `Activate`, `Component`, and `RawHtml`. The `{#activate}` block renders empty on the server (no Engram server available at SSR time); the client runtime re-runs semantic queries after hydration.
The `ServerBackend` target is the only target where hydration markers are injected: `data-el-component`, `data-el-id`, and `data-el-state` on each component root element.
#### `CodegenTarget::Native(Platform)` — Rust Native / WASM
Emits a Rust module where each component builds a `PlatformNode` tree via the semantic primitive layer and mounts it via the `el_platform` backend for the given platform.
Supported platforms:
| Platform | Backend | Output format |
|----------|---------|---------------|
| `Platform::Web` | `WebBackend` (web-sys) | Rust compiled to WASM — NOT JavaScript |
| `Platform::Ios` | `IosBackend` | Rust (objc2-ui-kit) |
| `Platform::Android` | `AndroidBackend` | Kotlin (Jetpack Compose via build.rs) |
| `Platform::Macos` | `MacosBackend` | Rust (objc2-app-kit) |
| `Platform::Linux` | `LinuxBackend` | Rust (gtk4-rs) |
| `Platform::Windows` | `WindowsBackend` | Rust (windows-rs / WinUI 3) |
**Note:** The `build_node_tree()` implementation in all native targets is currently a stub returning `PlatformNode::element("div")`. Full AST-to-semantic-primitive lowering is not yet wired in — the semantic module and per-platform codegen functions exist and are correct, but the compiler does not yet call them from the AST walk.
### 7.5 Semantic Primitive Layer (`semantic.rs`)
The semantic layer is the bridge between the template AST and platform-specific code generation. It defines EBD-driven concepts — *what* something is, not *how* it looks:
**AppearanceConcept** — what a control is:
- `Action` — primary action trigger (blue/filled on most platforms)
- `Destructive` — dangerous/irreversible action (red tint)
- `Secondary` — subdued / less-prominent action
- `Navigation` — link-like / back-navigation element
- `Informational` — read-only display element
- `Structural` — layout container with no interactive meaning
**LayoutConcept** — how children are arranged:
- `Stack` — vertical (Column / VStack / GtkBox vertical)
- `Row` — horizontal (Row / HStack / GtkBox horizontal)
- `Grid` — two-dimensional grid
- `Overlay` — z-axis layering (ZStack / GtkOverlay)
- `Scroll` — scrollable container
**TextConcept** — the semantic role of text:
- `Heading { level: u8 }` — heading at level 1–6
- `Body` — default body text
- `Caption` — small secondary text
- `Label` — control label
- `Code` — monospaced code text
**SemanticPrimitive** — the EBD building blocks:
- `Button { label, appearance, on_press }` — interactive button
- `Text { content, concept }` — text display
- `Container { children, layout }` — layout container
- `Input { binding, hint, appearance }` — text input bound to state
- `Image { src, alt }` — image element
- `Toggle { binding, label }` — checkbox/switch bound to boolean state
- `List { items_binding, item_name, item_template }` — homogeneous list
Appearance is inferred from explicit `appearance="..."` attributes, CSS class names, or HTML tag heuristics via `infer_appearance()`. Layout and text concepts are inferred from tag names via `infer_layout_concept()` and `infer_text_concept()`.
### 7.6 CLI
The `elc-ui` Node.js script in `vessels/el-ui-compiler/elc-ui` exposes the compiler as a command-line tool:
```bash
elc-ui App.el # compile to App.js (web target, default)
elc-ui App.el -o app.js # explicit output path
elc-ui App.el --target=server # emit Node.js render script
elc-ui App.el --target=server -o render.js
elc-ui App.el --runtime=../el-ui.js # custom runtime path for web target
```
**Web target** emits an ES2022 JavaScript module — the same class-per-component format as the hand-compiled examples in `examples/counter/app.js`. Requires the el-ui runtime (`el-ui.js` or `dist/el-ui.js`) to be available at the path specified by `--runtime`.
**Server target** emits a Node.js CJS module that:
- Embeds the component source at compile time (base64-encoded)
- Exports `renderToString(componentName, props)` → HTML string
- Exports `renderToDocument(componentName, props, shellOpts)` → full HTML document
- Exports `componentNames` → array of component names found in the source
- Supports CLI invocation: `node render.js ComponentName '{"prop":"val"}'`
The server render script is self-contained: it does not require re-reading the `.el` source file at render time. The El web server calls it via `exec()` and captures stdout as the rendered HTML string.
---
## 8. Runtime Architecture
### 8.1 Module Structure
| Module | Purpose |
|--------|---------|
| `graph.js` | Engram graph: nodes, edges, activation, search, subscribe, dump |
| `activation.js` | Standalone activation utilities: `spreadActivation`, `activationStrength`, `reachableNodes` |
| `renderer.js` | DOM patching, event binding |
| `router.js` | Graph-based routing |
| `index.js` | `Component` base class, `mount()`, `hydrate()`, re-exports |
| `ssr.js` | Server-side rendering engine (Node.js only, not a browser module) |
### 8.2 Component Lifecycle
**Client-side (mount path):**
| Lifecycle event | Description |
|-----------------|-------------|
| `constructor(props, initialState?)` | Seeds state graph, subscribes to activation. `initialState` is used by `hydrate()` to pre-load server state. |
| `render()` | Returns HTML string; called on activation |
| `onMount()` | Called after first render and DOM attachment |
**SSR + hydration path:**
| Lifecycle event | Description |
|-----------------|-------------|
| `constructor(props, initialState)` | Called by `hydrate()` with recovered server state |
| `_hydrate(root)` | Subscribes state nodes, creates renderer pointing at existing DOM, binds events. Does NOT call `render()`. |
| `onHydrate()` | Called after hydration completes |
**Note:** `onDestroy()` is documented in prior drafts but is not implemented in the base `Component` class or the `Renderer`. It will not be called by the runtime.
### 8.3 Mounting
```javascript
import { mount } from './el-ui.js';
import { App } from './app.js';
const component = mount(App, '#app', { optionalProp: 'value' });
// mount() returns the live Component instance
```
`mount` instantiates the root component, creates a `Renderer`, calls `renderer.mount()` which calls `render()`, sets `root.innerHTML`, binds events, then calls `onMount()`.
### 8.5 Hydration
```javascript
import { hydrate } from './el-ui.js';
import { Counter } from './app.js';
// Hydrate a server-rendered Counter without replacing the DOM
const component = hydrate('[data-el-component="Counter"]', Counter);
// or with a stable ID for multi-instance pages:
const component = hydrate('[data-el-id="el-counter-1"]', Counter);
```
`hydrate` finds the SSR root element, reads `data-el-state` to recover the server's initial state, constructs the component with that state pre-loaded, then calls `_hydrate(root)` which binds all event handlers without re-rendering. The existing server-rendered DOM is preserved; spreading activation begins on the first `setState()` call.
**SSR lifecycle:**
```
Server: renderToString(src, 'App', props) → HTML with markers
Network: HTML delivered, painted immediately (zero JS latency)
Client: import { hydrate } from './el-ui.js'
hydrate('[data-el-component="App"]', App)
→ events bound, spreading activation active
→ subsequent setState() calls patch the DOM normally
```
### 8.6 Server-Side Rendering
The SSR engine in `runtime/src/ssr.js` is a Node.js module that parses `.el` source files and renders components to HTML strings:
```javascript
const { renderToString, renderToDocument, parseComponents } = require('./runtime/src/ssr.js');
const fs = require('fs');
const src = fs.readFileSync('App.el', 'utf8');
// Render to HTML fragment
const html = renderToString(src, 'App', { title: 'Hello' });
// Render to full document
const doc = renderToDocument(src, 'App', { title: 'Hello' }, {
title: 'My App',
appScriptPath: './hydrate.js',
styles: 'body { background: #000; }',
});
```
**Hydration markers** emitted on every component root element:
| Attribute | Value | Purpose |
|-----------|-------|---------|
| `data-el-component` | component name | identifies component type for `hydrate()` |
| `data-el-id` | stable position-based ID | locates the exact instance when multiple components of the same type exist |
| `data-el-state` | JSON string | server's initial state snapshot; `hydrate()` reads this to initialize the Engram graph |
Event bindings are preserved as `data-el-*` attributes (e.g., `data-el-click="() => increment()"`). The client hydration pass binds these handlers via `new Function('__self', ...)` exactly as the standard mount path does.
**Server/client bridge:**
The El web server calls the render script as a subprocess:
```bash
node render.js ComponentName '{"prop":"value"}'
# stdout: the rendered HTML string
```
The render script is generated by `elc-ui --target=server`. It embeds the component source at compile time, so no `.el` file is needed at render time.
### 8.4 DOM Patching Strategy (v0.1)
The renderer uses full string re-render on every state change: `root.innerHTML = component.render()`. The activated node set argument to `patch()` is accepted but unused — targeted patching is planned for v0.2.
After patching, the renderer attempts to restore focus to the previously focused element by `id`.
Event handlers are rebound after every patch by scanning the new DOM for `data-el-{event}` attributes. Handlers are compiled via `new Function('__self', ...)` — this requires a permissive Content Security Policy in v0.1.
---
## 9. Platform Backend Architecture (`el-platform`)
The `el-platform` crate defines the `PlatformBackend` trait that all rendering backends implement:
```rust
pub trait PlatformBackend: Send + Sync {
fn name(&self) -> &'static str;
fn create_element(&self, tag: &str) -> PlatformResult;
fn create_text(&self, content: &str) -> PlatformResult;
fn set_attribute(&self, node: &mut PlatformNode, name: &str, value: &str) -> PlatformResult<()>;
fn remove_attribute(&self, node: &mut PlatformNode, name: &str) -> PlatformResult<()>;
fn append_child(&self, parent: &mut PlatformNode, child: PlatformNode) -> PlatformResult<()>;
fn remove_child(&self, parent: &mut PlatformNode, child_index: usize) -> PlatformResult<()>;
fn replace_child(&self, parent: &mut PlatformNode, index: usize, new_child: PlatformNode) -> PlatformResult<()>;
fn bind_event(&self, node: &mut PlatformNode, event: &str, handler: EventHandler) -> PlatformResult<()>;
fn render_to_string(&self, node: &PlatformNode) -> PlatformResult;
fn mount(&self, root: PlatformNode, container_id: &str) -> PlatformResult<()>;
fn patch(&self, old: &PlatformNode, new: &PlatformNode) -> PlatformResult<()>;
fn supports_ssr(&self) -> bool { false }
}
```
The `ServerBackend` is the only backend where `supports_ssr()` returns `true`. In v0.1, the server rendering implementation is in `runtime/src/ssr.js` (Node.js), not in a Rust crate. The `PlatformBackend` interface above describes the planned Rust abstraction; the current implementation fulfills the same contract via the JavaScript SSR engine, which is the authoritative server rendering path. All other backends are platform-native.
Platform is selected via `manifest.el`:
```toml
[platform]
target = "web" # web | server | ios | android | macos | linux | windows
ssr = true
```
---
## 10. Comparison to Other Frameworks
### 10.1 vs. React
React uses virtual DOM diffing: on every render, it constructs a virtual tree and diffs it against the previous tree to identify minimal DOM mutations. The unit of re-render is the component; re-render triggers are driven by `setState`, `useReducer`, or context changes.
el-ui does not compute a virtual DOM. The spreading activation pass determines the re-render set from the graph topology. State relationships that exist in the graph are automatically propagated; relationships that do not exist are not. There is no need to declare dependencies, use `useMemo`, or prevent unnecessary renders with `React.memo` — the graph structure encodes the dependency information directly.
### 10.2 vs. Vue
Vue uses a Proxy-based reactive system: state values are wrapped in reactive proxies, and reads from these proxies during render are tracked as dependencies. When a reactive value changes, all components that read it during their last render are re-rendered.
el-ui's graph model is explicit: relationships between state nodes are declared via graph edges, not inferred from read tracking. This makes dependencies inspectable and modifiable at runtime. It also enables semantic queries (`{#activate}`) that find relevant state by meaning rather than by structural reference.
### 10.3 vs. Svelte
Svelte uses compile-time analysis to determine which parts of the DOM update when which state changes. The compiler emits targeted DOM mutations. There is no runtime overhead for a virtual DOM or a reactive system.
el-ui uses a runtime spreading activation pass. This is more expensive than Svelte's compile-time approach for small, known-static state graphs. The advantage is dynamic state topology: new nodes and edges can be added at runtime without recompilation, enabling state graphs that evolve with program execution.
### 10.4 Core Differentiator
No prior framework uses Hebbian spreading activation as the re-render decision mechanism. The multiplicative activation formula, the self-seeded activation from a root node, the importance-boost on state change (LTP analog), and the semantic `{#activate}` query construct have no analogues in any shipping frontend framework as of April 2026.
---
## 11. SSR + Hydration Example
A complete SSR + hydration example lives in `examples/ssr-counter/`.
### 11.1 Counter.el
The component definition with state, methods, `{#if}` conditionals, and child composition:
```
component Counter {
props {
label: String = "Counter"
}
state {
count: Int = 0
history: String = ""
}
fn increment() -> Void {
count = count + 1
history = history + "+" + count + " "
}
template {
}
}
```
### 11.2 Compile
```bash
# Server render script (embed source, export renderToString)
elc-ui Counter.el --target=server -o render.js
# Browser module (class-per-component, for hydration)
elc-ui Counter.el --target=web --runtime=../../dist/el-ui.js -o app.js
```
### 11.3 Server Render
```bash
# Render to HTML fragment
node render.js Counter
# =>
#
Counter
# 0
# +
#
Press + or - to begin.
#
```
Or from Node.js:
```javascript
const { renderToString } = require('./render.js');
const html = renderToString('Counter', { label: 'Score' });
```
### 11.4 Client Hydration
```javascript
// hydrate.js — runs in browser after SSR HTML is painted
import { hydrate } from '../../dist/el-ui.js';
import { Counter } from './app.js';
// Find each server-rendered Counter and attach the runtime
document.querySelectorAll('[data-el-component="Counter"]').forEach(el => {
if (el.dataset.elHydrated) return;
hydrate(`[data-el-id="${el.dataset.elId}"]`, Counter);
el.dataset.elHydrated = 'true';
});
```
### 11.5 Full Lifecycle
```
1. Server calls: node render.js App
2. Server injects: rendered HTML into
3. Browser: HTML painted immediately (no JS needed for first paint)
4. Browser: