<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Judah Sullivan</title>
    <description>The latest articles on Forem by Judah Sullivan (@judahbsullivan).</description>
    <link>https://forem.com/judahbsullivan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3703482%2F937785bf-17e7-4c9b-b20d-94bd47b71239.png</url>
      <title>Forem: Judah Sullivan</title>
      <link>https://forem.com/judahbsullivan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/judahbsullivan"/>
    <language>en</language>
    <item>
      <title>I Built a UI Framework That Doesn’t Use a Virtual DOM</title>
      <dc:creator>Judah Sullivan</dc:creator>
      <pubDate>Fri, 23 Jan 2026 23:53:29 +0000</pubDate>
      <link>https://forem.com/judahbsullivan/i-built-a-ui-framework-that-doesnt-use-a-virtual-dom-4cap</link>
      <guid>https://forem.com/judahbsullivan/i-built-a-ui-framework-that-doesnt-use-a-virtual-dom-4cap</guid>
      <description>&lt;h1&gt;
  
  
  I Built a UI Framework That Doesn’t Use a Virtual DOM
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introducing &lt;strong&gt;Zenith&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnmrrg5cbmnmaaqsi2l0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxnmrrg5cbmnmaaqsi2l0.png" alt=" " width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Over the last few years, I’ve worked extensively with React, Vue, Next, Nuxt, Svelte, and compiler-adjacent tooling. Each of them solves real problems — but they also left me with a recurring question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What if the compiler already knew everything before runtime ever existed?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question is what led me to build &lt;strong&gt;Zenith&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Zenith is a &lt;strong&gt;compiler-first UI framework&lt;/strong&gt; that intentionally does &lt;strong&gt;not&lt;/strong&gt; use a Virtual DOM, runtime diffing, or reactive boundaries. Not because those tools are bad — but because I wanted to explore what becomes possible when &lt;strong&gt;certainty replaces reconciliation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is an announcement, not a sales pitch. Zenith is different by design, and that difference is intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Started Building Zenith
&lt;/h2&gt;

&lt;p&gt;Most modern UI frameworks share a common assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UI updates are a &lt;em&gt;runtime problem&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;State changes, components re-run, virtual trees are recreated, and the framework figures out what changed &lt;em&gt;after the fact&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Zenith flips that assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UI updates are a &lt;strong&gt;compile-time problem&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of asking &lt;em&gt;“what changed?”&lt;/em&gt; at runtime, Zenith asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“What could ever change?”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
…and answers that &lt;strong&gt;before runtime exists&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that answer is known, there’s nothing left to diff.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkbrsthc8parma5zl7um.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkbrsthc8parma5zl7um.PNG" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Zenith Is (and Isn’t)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Zenith is:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compiler-driven&lt;/li&gt;
&lt;li&gt;Structural by default&lt;/li&gt;
&lt;li&gt;Predictable and explicit&lt;/li&gt;
&lt;li&gt;Minimal at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Zenith is not:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Virtual DOM framework&lt;/li&gt;
&lt;li&gt;A reactive runtime&lt;/li&gt;
&lt;li&gt;A template engine&lt;/li&gt;
&lt;li&gt;A drop-in replacement for React or Vue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a different axis entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Simple Counter Example
&lt;/h2&gt;

&lt;p&gt;Let’s start with something familiar.&lt;/p&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Count: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works great — but under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component function re-executes&lt;/li&gt;
&lt;li&gt;A new Virtual DOM tree is created&lt;/li&gt;
&lt;li&gt;React diffs old vs new&lt;/li&gt;
&lt;li&gt;Then updates the DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All decisions happen &lt;strong&gt;at runtime&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Zenith
&lt;/h3&gt;

&lt;p&gt;Zenith enforces a strict separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behavior lives in &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markup only declares bindings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No inline expressions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No directives&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No runtime interpretation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Count: {count}
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzaflvjwtgj1ehtqtrhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzaflvjwtgj1ehtqtrhb.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Compiler Does
&lt;/h2&gt;

&lt;p&gt;At &lt;strong&gt;compile time&lt;/strong&gt;, Zenith:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resolves &lt;code&gt;count&lt;/code&gt; as a state cell&lt;/li&gt;
&lt;li&gt;Resolves &lt;code&gt;increment&lt;/code&gt; as a function reference&lt;/li&gt;
&lt;li&gt;Determines &lt;code&gt;{count}&lt;/code&gt; affects a single text node&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Emits exact instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One DOM event listener&lt;/li&gt;
&lt;li&gt;One direct DOM text update&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;At &lt;strong&gt;runtime&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The function runs&lt;/li&gt;
&lt;li&gt;State mutates&lt;/li&gt;
&lt;li&gt;The known DOM node updates directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is &lt;strong&gt;no re-render path&lt;/strong&gt;.&lt;br&gt;
There is &lt;strong&gt;no diffing step&lt;/strong&gt;.&lt;br&gt;
There is &lt;strong&gt;nothing to guess&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why &lt;code&gt;{}&lt;/code&gt; Exists in Zenith
&lt;/h2&gt;

&lt;p&gt;In Zenith, &lt;code&gt;{}&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; mean “evaluate an expression.”&lt;/p&gt;

&lt;p&gt;It means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Bind this node to a compiler-tracked value.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s why these are &lt;strong&gt;invalid&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{count + 1}
{someFunction()}
onclick={count++}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those require runtime evaluation — and Zenith rejects them at compile time.&lt;/p&gt;

&lt;p&gt;The compiler must be certain, or it fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Components in Zenith Are Structural
&lt;/h2&gt;

&lt;p&gt;One of Zenith’s core constraints is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Components do not introduce behavior or reactivity.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They are purely &lt;strong&gt;structural transforms&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No lifecycle hooks&lt;/li&gt;
&lt;li&gt;No hidden scopes&lt;/li&gt;
&lt;li&gt;No reactive boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slot content to retain its original scope&lt;/li&gt;
&lt;li&gt;State identity to remain intact&lt;/li&gt;
&lt;li&gt;The compiler to reason globally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This constraint is not accidental — it’s what makes the model possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why No Virtual DOM?
&lt;/h2&gt;

&lt;p&gt;The Virtual DOM is an elegant solution to a hard problem.&lt;/p&gt;

&lt;p&gt;Zenith avoids the problem entirely.&lt;/p&gt;

&lt;p&gt;VDOM frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recompute → then optimize&lt;/li&gt;
&lt;li&gt;Recover from uncertainty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zenith:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevents uncertainty&lt;/li&gt;
&lt;li&gt;Emits intent, not guesses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not “better” — it’s &lt;strong&gt;different by construction&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;p&gt;Zenith is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actively developed&lt;/li&gt;
&lt;li&gt;Compiler-driven (Rust)&lt;/li&gt;
&lt;li&gt;Early, opinionated, and evolving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not production-ready yet — and that’s okay.&lt;/p&gt;

&lt;p&gt;Right now, Zenith is about exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What a compiler can guarantee&lt;/li&gt;
&lt;li&gt;How much runtime can be eliminated&lt;/li&gt;
&lt;li&gt;What UI looks like without reactive boundaries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I’m Sharing This Now
&lt;/h2&gt;

&lt;p&gt;Zenith started as a question.&lt;/p&gt;

&lt;p&gt;It grew into a compiler.&lt;/p&gt;

&lt;p&gt;Now it’s a framework.&lt;/p&gt;

&lt;p&gt;I’m sharing it early because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I value architectural discussion&lt;/li&gt;
&lt;li&gt;I want ideas challenged&lt;/li&gt;
&lt;li&gt;And I believe UI tooling still has unexplored space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re curious, skeptical, or opinionated — that’s exactly the audience I want to hear from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Zenith isn’t trying to replace React or Vue.&lt;/p&gt;

&lt;p&gt;It’s asking a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What if the compiler already knew everything?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks for reading — more to come.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
    </item>
  </channel>
</rss>
