14 KiB
DreamStack: Reinventing the UI from First Principles
What if we threw away every assumption about web UI frameworks and started over — with the sole goal of creating ultra-reactive, dynamic interfaces?
Vision
React was revolutionary in 2013. But it carries a decade of compromises: the virtual DOM, hooks with manual dependency arrays, re-rendering entire subtrees, CSS from 1996, animations bolted on as an afterthought. DreamStack asks: what would we build today if none of that existed?
The answer is a unified system where UI is data, reactivity is automatic, effects are composable values, layout is constraint-based, animation is physics-native, and the editor and runtime are one.
The Language
Not Clojure. Not TypeScript. A new language that steals the best ideas from everywhere.
Core Properties
| Property | Inspiration | Why |
|---|---|---|
| Homoiconic | Clojure, Lisp | UI = data. Code = data. Everything is transformable |
| Dependent types | Idris, Agda | Types that express "this button is disabled when the form is invalid" at the type level |
| Algebraic effects | Eff, Koka, OCaml 5 | Side effects as first-class, composable, interceptable values |
| Reactive by default | Svelte, Solid, Excel | No useState, no subscriptions. Values are reactive. Assignment is the API |
| Structural typing | TypeScript, Go | Flexible composition without class hierarchies |
| Compiled to native + WASM | Rust, Zig | Near-zero runtime overhead. No GC pauses during animations |
Syntax
-- Signals are the primitive. Assignment propagates automatically.
let count = 0
let doubled = count * 2 -- derived, auto-tracked
let label = "Count: {doubled}" -- derived, auto-tracked
-- UI is data. No JSX, no templates, no virtual DOM.
view counter =
column [
text label -- auto-updates when count changes
button "+" { click: count += 1 }
-- Conditional UI is just pattern matching on signals
when count > 10 ->
text "🔥 On fire!" | animate fade-in 200ms
]
No hooks. No dependency arrays. No re-renders. The compiler builds a fine-grained reactive graph at compile time.
Reactivity: Compile-Time Signal Graph
This is the biggest departure from React. React's model is fundamentally pull-based — re-render the tree, diff it, patch the DOM. That's backwards.
Push-Based with Compile-Time Analysis
Signal: count
├──► Derived: doubled
│ └──► Derived: label
│ └──► [direct DOM binding] TextNode #47
└──► Condition: count > 10
└──► [mount/unmount] text "🔥 On fire!"
Key principles:
- No virtual DOM. The compiler knows at build time exactly which DOM nodes depend on which signals. When
countchanges, it updates only the specific text node and evaluates the condition. Nothing else runs. - No re-rendering. Components don't re-execute. They execute once and set up reactive bindings.
- No dependency arrays. The compiler infers all dependencies from the code. You literally cannot forget one.
- No stale closures. Since there are no closures over render cycles, the entire class of bugs vanishes.
This extends Solid.js's approach with full compile-time analysis in a purpose-built language.
Effect System: Algebraic Effects for Everything
Every side-effect — HTTP, animations, time, user input, clipboard, drag-and-drop — is an algebraic effect. Effects are values you can compose, intercept, retry, and test.
-- An effect is declared, not executed
effect fetchUser(id: UserId): Result<User, ApiError>
-- A component "performs" an effect — doesn't control HOW it runs
view profile(id: UserId) =
let user = perform fetchUser(id)
match user
Loading -> skeleton-loader
Ok(u) -> column [
avatar u.photo
text u.name
]
Err(e) -> error-card e | with retry: perform fetchUser(id)
-- At the app boundary, you provide the HANDLER
handle app with
fetchUser(id, resume) ->
let result = http.get "/api/users/{id}"
resume(result)
Why This is Powerful
- Testing: Swap the handler.
handle app with mockFetchUser(...)— no mocking libraries, no dependency injection frameworks. - Composition: Effects compose naturally. An animation effect + a data-fetch effect combine without callback hell or
useEffectchains. - Interceptors: Want to add logging, caching, or retry logic? Add a handler layer. The component code never changes.
- Time travel: Since effects are values, you can record and replay them. Free undo/redo. Free debugging.
Data Flow: Everything is a Stream
Forget the distinction between "state", "props", "events", and "side effects". Everything is a stream of values over time.
-- User input is a stream
let clicks = stream from button.click
let keypresses = stream from input.keydown
-- Derived streams with temporal operators
let search_query = keypresses
| map .value
| debounce 300ms
| distinct
-- API results are streams
let results = search_query
| flatmap (q -> http.get "/search?q={q}")
| catch-with []
-- UI binds to streams directly
view search =
column [
input { on-keydown: keypresses }
match results
Loading -> spinner
Data(rs) -> list rs (r -> search-result-card r)
Empty -> text "No results"
]
Unification
This single abstraction covers everything:
| Concept | Traditional | DreamStack |
|---|---|---|
| User input | Event handlers | Stream |
| Network responses | Promises / async-await | Stream |
| Animations | CSS transitions / JS libraries | Stream of interpolated values |
| Timers | setInterval / setTimeout |
Stream |
| WebSockets | Callback-based | Stream |
| Drag events | Complex event handler state machines | Stream of positions |
One abstraction. One composition model. Everything snaps together.
Layout: Constraint-Based, Not Box-Based
CSS's box model is from 1996. Flexbox and Grid are patches on a fundamentally limited system. DreamStack starts over with a constraint solver.
-- Declare relationships, not boxes
layout dashboard =
let sidebar.width = clamp(200px, 20vw, 350px)
let main.width = viewport.width - sidebar.width
let header.height = 64px
let content.height = viewport.height - header.height
-- Constraints, not nesting
sidebar.left = 0
sidebar.top = header.height
sidebar.height = content.height
main.left = sidebar.right
main.top = header.height
main.right = viewport.right
-- Responsive is just different constraints
when viewport.width < 768px ->
sidebar.width = 0 -- collapses
main.left = 0 -- takes full width
Inspired by Apple's Auto Layout (Cassowary constraint solver), but made reactive. When viewport.width changes, the constraint solver re-solves and updates positions in a single pass. No layout thrashing. No "CSS specificity wars."
Advantages Over CSS
- No cascade conflicts — constraints are explicit and local
- No z-index hell — layering is declarative
- No media query breakpoints — responsiveness emerges from constraints
- Animations are free — animating a constraint target is the same as setting it
Animation: First-Class, Physics-Based, Interruptible
Animations aren't CSS transitions bolted on after the fact. They're part of the reactive graph.
-- A spring is a signal that animates toward its target
let panel_x = spring(target: 0, stiffness: 300, damping: 30)
-- Change the target → the spring animates automatically
on toggle_sidebar ->
panel_x.target = if open then 250 else 0
-- Gestures feed directly into springs
on drag(event) ->
panel_x.target = event.x -- spring follows finger
on drag-end(event) ->
panel_x.target = snap-to-nearest [0, 250] event.x
-- UI just reads the spring's current value
view sidebar =
panel { x: panel_x } [
nav-items
]
Design Principles
- Interruptible: Start a new animation mid-flight. The spring handles the physics — no jarring jumps, no "wait for the current animation to finish."
- Gesture-driven: Touch/mouse input feeds directly into the animation model. No separate "gesture handler" → "state update" → "CSS transition" pipeline.
- 60fps guaranteed: Springs resolve on the GPU. The main thread never blocks.
- Composable: Combine springs, easing curves, and physics simulations using the same stream operators as everything else.
Runtime: Tiny, Compiled, No GC Pauses
┌──────────────────────────────────────────┐
│ Compiled Output │
├──────────────────────────────────────────┤
│ Signal Graph (static DAG) ~2KB │
│ Constraint Solver (layout) ~4KB │
│ Spring Physics (animations) ~1KB │
│ Effect Runtime (handlers) ~2KB │
│ DOM Patcher (surgical updates) ~3KB │
├──────────────────────────────────────────┤
│ Total Runtime: ~12KB │
│ (vs React ~45KB + ReactDOM ~130KB) │
└──────────────────────────────────────────┘
The compiler does the heavy lifting. The runtime is tiny because there is:
- No virtual DOM diffing algorithm
- No fiber scheduler
- No hook state management system
- No reconciliation algorithm
- No garbage collector pauses during animation frames
The Killer Feature: Live Structural Editing
Because the language is homoiconic (code = data), the editor IS the runtime:
┌─────────────────────────────────┐
│ Live Editor │
│ │
│ view counter = │ ← Edit this...
│ column [ │
│ text "Count: {count}" │
│ button "+" { ... } │
│ ] │
│ │
│ ───────────────────────────── │
│ │
│ ┌─────────────┐ │ ← ...see this update
│ │ Count: 42 │ │ instantly, with state
│ │ [ + ] │ │ preserved.
│ └─────────────┘ │
│ │
└─────────────────────────────────┘
- Drag and drop UI elements in the preview → the code updates.
- Edit the code → the preview updates.
- Both directions, simultaneously, with state preserved.
- Not a design tool that generates code — the code IS the design tool.
This is possible because:
- Homoiconicity means UI structure is inspectable and modifiable data
- Immutable signals mean state survives code changes
- Compile-time signal graph means changes are surgical, not full-page reloads
Comparison to Existing Approaches
| Capability | React | Svelte 5 | Solid.js | Flutter | DreamStack |
|---|---|---|---|---|---|
| Reactivity | Pull (re-render + diff) | Compile-time runes | Fine-grained signals | Widget rebuild | Compile-time signal DAG |
| Side effects | useEffect + deps array |
$effect |
createEffect |
Lifecycle methods | Algebraic effects |
| Layout | CSS (external) | CSS (scoped) | CSS (external) | Constraint-based | Constraint solver (native) |
| Animation | 3rd party libs | 3rd party libs | 3rd party libs | Physics-based (native) | Physics-based (native) |
| SSR | Yes (complex) | Yes | Yes | No (web) | Yes (compiled) |
| Runtime size | ~175KB | ~2KB | ~7KB | ~2MB (web) | ~12KB |
| Live editing | Fast Refresh (lossy) | HMR | HMR | Hot reload | Bidirectional structural |
| Type safety | TypeScript (bolt-on) | TypeScript (bolt-on) | TypeScript (bolt-on) | Dart (native) | Dependent types (native) |
| UI = Data | No (JSX compiled away) | No (templates) | No (JSX compiled) | No (widget classes) | Yes (homoiconic) |
Fragments of the Future, Today
The closest approximations to pieces of this vision:
- Solid.js — fine-grained reactivity, no VDOM, ~7KB runtime
- Svelte 5 Runes — compiler-driven reactivity, tiny output
- Elm — algebraic effects-adjacent, immutable state, strong types
- Flutter — constraint layout, physics-based animation, hot reload
- Clojure/Reagent — homoiconicity, UI as data, ratom reactivity
- Koka — algebraic effect system in a practical language
- Apple Auto Layout — Cassowary constraint solver for UI
- Excel — reactive by default, dependency auto-tracking
Nobody has unified them. That's the opportunity.
Design Philosophy
- The compiler is the framework. Move work from runtime to compile time. The less code that runs in the browser, the faster the UI.
- Reactivity is not a feature, it's the default. Every value is live. Every binding is automatic. You opt out of reactivity, not in.
- Effects are values, not side-channels. Making side effects first-class and composable eliminates the largest source of bugs in modern UIs.
- Layout and animation are not afterthoughts. They're core primitives, not CSS bolt-ons or third-party libraries.
- The editor and the runtime are the same thing. Bidirectional editing collapses the design-develop gap entirely.
- UI is data, all the way down. If you can't
mapover your UI structure, your abstraction is wrong.