<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The MorphScreen blog</title>
<link>https://morphscreen.com/blog</link>
<description>How MorphScreen works and what we measured.</description>
<language>en</language>
<atom:link href="https://morphscreen.com/blog.xml" rel="self" type="application/rss+xml"/>
<item>
<title>Checking a model&apos;s UI line by line while it streams</title>
<link>https://morphscreen.com/blog/checking-ui-while-it-streams</link>
<guid isPermaLink="true">https://morphscreen.com/blog/checking-ui-while-it-streams</guid>
<pubDate>Fri, 06 Nov 2026 00:00:00 GMT</pubDate>
<description><![CDATA[<p>MorphScreen checks a model&#39;s UI one line at a time while the answer is still streaming. A small Rust core reads each finished line against the components the app allows, the page draws the good lines straight away, and only the broken ones go back to the same model to be rewritten. On OpenUI&#39;s benchmark briefs, that took DeepSeek V4 Flash from 87.0% valid answers to 184 of 184.</p>
<figure class="doc-video">
<video data-src="/docs/media/intro.mp4" poster="/docs/media/intro.jpg" muted loop playsinline preload="none" aria-label="An answer about the planets streaming into a page with photos, a chart, products, a quiz and a form"></video>
<figcaption>A saved DeepSeek V4 Flash answer streaming through the web SDK at the pace the model wrote it, unedited.</figcaption>
</figure><h2 id="the-problem-with-waiting-for-the-end"><a class="anchor" href="#the-problem-with-waiting-for-the-end" aria-hidden="true" tabindex="-1">#</a>The problem with waiting for the end</h2>
<p>When a model answers with UI instead of text, drawing it isn&#39;t the hard part. The hard part is what to do with the bits that are wrong while the answer is still arriving.</p>
<p>And models get UI wrong all the time. They invent a component your app doesn&#39;t have, add an attribute that doesn&#39;t exist, or close a tag with its parent&#39;s name. With a JSON format you usually find out at the end, when the whole thing fails to parse, and the reader has been looking at a spinner the whole time.</p>
<h2 id="why-one-line-at-a-time"><a class="anchor" href="#why-one-line-at-a-time" aria-hidden="true" tabindex="-1">#</a>Why one line at a time</h2>
<p>The model writes Markdown with a few tags. Here&#39;s a short answer, and what the reader sees:</p>
<div class="tabs" data-tabs="preview"><div class="tabs__list" role="tablist"><button type="button" role="tab" id="tab-1-0" aria-controls="panel-1-0" aria-selected="true" data-tab="Preview">Preview</button><button type="button" role="tab" id="tab-1-1" aria-controls="panel-1-1" aria-selected="false" data-tab="Source">Source</button></div><div class="tabs__panel" role="tabpanel" id="panel-1-0" aria-labelledby="tab-1-0" data-tab="Preview"><div class="preview"><div class="ms-answer"><h2>Weekend <span class="ms-pair">in Lisbon</span></h2>
<div class="ms-cols ms-n2 ms-m140"><div class="ms-cell"><div class="ms-stat"><span class="ms-label">Budget</span><span class="ms-value">$330</span><span class="ms-change"></span><span class="ms-note"></span></div></div><div class="ms-cell"><div class="ms-stat"><span class="ms-label">Per day</span><span class="ms-value">$165</span><span class="ms-change"></span><span class="ms-note"></span></div></div></div>
<div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>Where <span class="ms-pair">it goes</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="Where it goes" data-kind="bar" data-l="40" data-t="8" data-pw="652.0" data-ph="240.0" data-lo="0" data-hi="200" data-n="3"><line class="ms-base" x1="40" x2="692" y1="248.0" y2="248.0"/><text x="32" y="252.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="692" y1="188.0" y2="188.0"/><text x="32" y="192.0" text-anchor="end">50</text><line class="ms-grid" x1="40" x2="692" y1="128.0" y2="128.0"/><text x="32" y="132.0" text-anchor="end">100</text><line class="ms-grid" x1="40" x2="692" y1="68.0" y2="68.0"/><text x="32" y="72.0" text-anchor="end">150</text><line class="ms-grid" x1="40" x2="692" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">200</text><text x="148.7" y="268" text-anchor="middle">Stay</text><text x="366.0" y="268" text-anchor="middle">Food</text><text x="583.3" y="268" text-anchor="middle">Trams</text><rect class="ms-s1" data-i="0" x="130.7" y="32.0" width="34.0" height="216.0" rx="2"/><rect class="ms-s1" data-i="1" x="348.0" y="140.0" width="34.0" height="108.0" rx="2"/><rect class="ms-s1" data-i="2" x="565.3" y="176.0" width="34.0" height="72.0" rx="2"/></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>Where <span class="ms-pair">it goes</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="Where it goes" data-kind="bar" data-l="40" data-t="8" data-pw="332.0" data-ph="125.0" data-lo="0" data-hi="200" data-n="3"><line class="ms-base" x1="40" x2="372" y1="133.0" y2="133.0"/><text x="32" y="137.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="372" y1="101.8" y2="101.8"/><text x="32" y="105.8" text-anchor="end">50</text><line class="ms-grid" x1="40" x2="372" y1="70.5" y2="70.5"/><text x="32" y="74.5" text-anchor="end">100</text><line class="ms-grid" x1="40" x2="372" y1="39.3" y2="39.3"/><text x="32" y="43.3" text-anchor="end">150</text><line class="ms-grid" x1="40" x2="372" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">200</text><text x="95.3" y="153" text-anchor="middle">Stay</text><text x="206.0" y="153" text-anchor="middle">Food</text><text x="316.7" y="153" text-anchor="middle">Trams</text><rect class="ms-s1" data-i="0" x="77.3" y="20.5" width="34.0" height="112.5" rx="2"/><rect class="ms-s1" data-i="1" x="188.0" y="76.8" width="34.0" height="56.3" rx="2"/><rect class="ms-s1" data-i="2" x="298.7" y="95.5" width="34.0" height="37.5" rx="2"/></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Where <span class="ms-pair">it goes</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Where it goes" data-kind="bar" data-l="40" data-t="8" data-pw="212.0" data-ph="82.0" data-lo="0" data-hi="200" data-n="3"><line class="ms-base" x1="40" x2="252" y1="90.0" y2="90.0"/><text x="32" y="94.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="252" y1="69.5" y2="69.5"/><text x="32" y="73.5" text-anchor="end">50</text><line class="ms-grid" x1="40" x2="252" y1="49.0" y2="49.0"/><text x="32" y="53.0" text-anchor="end">100</text><line class="ms-grid" x1="40" x2="252" y1="28.5" y2="28.5"/><text x="32" y="32.5" text-anchor="end">150</text><line class="ms-grid" x1="40" x2="252" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">200</text><text x="75.3" y="110" text-anchor="middle">Stay</text><text x="146.0" y="110" text-anchor="middle">Food</text><text x="216.7" y="110" text-anchor="middle">Trams</text><rect class="ms-s1" data-i="0" x="57.3" y="16.2" width="34.0" height="73.8" rx="2"/><rect class="ms-s1" data-i="1" x="128.0" y="53.1" width="34.0" height="36.9" rx="2"/><rect class="ms-s1" data-i="2" x="198.7" y="65.4" width="34.0" height="24.6" rx="2"/></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Where it goes</caption><thead><tr><th scope="col">Item</th><th scope="col">USD</th></tr></thead><tbody><tr><th scope="row">Stay</th><td>180</td></tr><tr><th scope="row">Food</th><td>90</td></tr><tr><th scope="row">Trams</th><td>60</td></tr></tbody></table></div></div>
</div></div></div><div class="tabs__panel" role="tabpanel" id="panel-1-1" aria-labelledby="tab-1-1" data-tab="Source"><figure class="cb" data-lang="markdown"><figcaption class="cb__head"><span class="cb__title">The model's answer</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t3">##</span><span class="t1"> Weekend in Lisbon</span><p><span class="t3">&lt;</span><span class="t1">Columns</span><span class="t3">&gt;</span>
<span class="t3">&lt;</span><span class="t1">Stat</span><span class="t2"> </span><span class="t1">label</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Budget</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">value</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">$330</span><span class="t3">&quot;</span><span class="t3"> /&gt;</span>
<span class="t3">&lt;</span><span class="t1">Stat</span><span class="t2"> </span><span class="t1">label</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Per day</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">value</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">$165</span><span class="t3">&quot;</span><span class="t3"> /&gt;</span>
<span class="t3">&lt;/</span><span class="t1">Columns</span><span class="t3">&gt;</span></p>
<p><span class="t3">&lt;</span><span class="t1">Chart</span><span class="t2"> </span><span class="t1">type</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">bar</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">title</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Where it goes</span><span class="t3">&quot;</span><span class="t3">&gt;</span>
<span class="t2">| Item | USD |</span>
<span class="t2">|---|---|</span>
<span class="t2">| Stay | 180 |</span>
<span class="t2">| Food | 90 |</span>
<span class="t2">| Trams | 60 |</span>
<span class="t3">&lt;/</span><span class="t1">Chart</span><span class="t3">&gt;</span></code></pre></figure></div></div></p>
<p>Models already write Markdown well, and a tag per line is easy for them to get right. It also means half an answer is always a valid start of the whole one. There&#39;s no closing bracket to wait for, so the page draws the heading and the first stat before the model has written the second.</p>
<h2 id="what-happens-to-each-line"><a class="anchor" href="#what-happens-to-each-line" aria-hidden="true" tabindex="-1">#</a>What happens to each line</h2>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><ol class="ms-steps"><li class="ms-done"><strong>The model finishes <span class="ms-pair">a line</span></strong><p>A heading, a table row, or one tag such as a Stat.</p></li><li class="ms-done"><strong>The core <span class="ms-pair">checks it</span></strong><p>Against the components the app allows: their names, their attributes, the values each takes and where each may go.</p></li><li class="ms-done"><strong>A good line is drawn <span class="ms-pair">at once</span></strong><p>The page updates while the model keeps writing.</p></li><li class="ms-done"><strong>A broken line <span class="ms-pair">is repaired</span></strong><p>Only that line goes back to the same model, with what's wrong with it.</p></li></ol>
</div></div><figcaption>Every finished line takes one of two paths. This figure, like the others in the post, is MorphScreen drawing the Markdown it was written in.</figcaption></figure><p>The checking happens in a small Rust core, <code>no_std</code>, compiled to WebAssembly for the browser (about 219 KB compressed) and linked natively for SwiftUI and Compose. It knows the app&#39;s catalog: 75 built-in components in packs, plus whatever the app registers itself.</p>
<h2 id="lenient-where-the-intent-is-clear"><a class="anchor" href="#lenient-where-the-intent-is-clear" aria-hidden="true" tabindex="-1">#</a>Lenient where the intent is clear</h2>
<p>Plenty of mistakes have an obvious meaning, and the core reads them that way instead of failing. Here a model wrote <code>tone=&quot;error&quot;</code> where the component says <code>danger</code>, and <code>type=&quot;column&quot;</code> where it says <code>bar</code>:</p>
<div class="tabs" data-tabs="preview"><div class="tabs__list" role="tablist"><button type="button" role="tab" id="tab-2-0" aria-controls="panel-2-0" aria-selected="true" data-tab="Preview">Preview</button><button type="button" role="tab" id="tab-2-1" aria-controls="panel-2-1" aria-selected="false" data-tab="Source">Source</button></div><div class="tabs__panel" role="tabpanel" id="panel-2-0" aria-labelledby="tab-2-0" data-tab="Preview"><div class="preview"><div class="ms-answer"><aside class="ms-callout ms-danger"><strong>Payment failed</strong><p>The card was declined. Try another one.</p></aside>
<div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>Sign-ups <span class="ms-pair">this week</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="Sign-ups this week" data-kind="bar" data-l="40" data-t="8" data-pw="652.0" data-ph="240.0" data-lo="0" data-hi="20" data-n="3"><line class="ms-base" x1="40" x2="692" y1="248.0" y2="248.0"/><text x="32" y="252.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="692" y1="188.0" y2="188.0"/><text x="32" y="192.0" text-anchor="end">5</text><line class="ms-grid" x1="40" x2="692" y1="128.0" y2="128.0"/><text x="32" y="132.0" text-anchor="end">10</text><line class="ms-grid" x1="40" x2="692" y1="68.0" y2="68.0"/><text x="32" y="72.0" text-anchor="end">15</text><line class="ms-grid" x1="40" x2="692" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">20</text><text x="148.7" y="268" text-anchor="middle">Mon</text><text x="366.0" y="268" text-anchor="middle">Tue</text><text x="583.3" y="268" text-anchor="middle">Wed</text><rect class="ms-s1" data-i="0" x="130.7" y="104.0" width="34.0" height="144.0" rx="2"/><rect class="ms-s1" data-i="1" x="348.0" y="32.0" width="34.0" height="216.0" rx="2"/><rect class="ms-s1" data-i="2" x="565.3" y="140.0" width="34.0" height="108.0" rx="2"/></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>Sign-ups <span class="ms-pair">this week</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="Sign-ups this week" data-kind="bar" data-l="40" data-t="8" data-pw="332.0" data-ph="125.0" data-lo="0" data-hi="20" data-n="3"><line class="ms-base" x1="40" x2="372" y1="133.0" y2="133.0"/><text x="32" y="137.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="372" y1="101.8" y2="101.8"/><text x="32" y="105.8" text-anchor="end">5</text><line class="ms-grid" x1="40" x2="372" y1="70.5" y2="70.5"/><text x="32" y="74.5" text-anchor="end">10</text><line class="ms-grid" x1="40" x2="372" y1="39.3" y2="39.3"/><text x="32" y="43.3" text-anchor="end">15</text><line class="ms-grid" x1="40" x2="372" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">20</text><text x="95.3" y="153" text-anchor="middle">Mon</text><text x="206.0" y="153" text-anchor="middle">Tue</text><text x="316.7" y="153" text-anchor="middle">Wed</text><rect class="ms-s1" data-i="0" x="77.3" y="58.0" width="34.0" height="75.0" rx="2"/><rect class="ms-s1" data-i="1" x="188.0" y="20.5" width="34.0" height="112.5" rx="2"/><rect class="ms-s1" data-i="2" x="298.7" y="76.8" width="34.0" height="56.3" rx="2"/></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Sign-ups <span class="ms-pair">this week</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Sign-ups this week" data-kind="bar" data-l="40" data-t="8" data-pw="212.0" data-ph="82.0" data-lo="0" data-hi="20" data-n="3"><line class="ms-base" x1="40" x2="252" y1="90.0" y2="90.0"/><text x="32" y="94.0" text-anchor="end">0</text><line class="ms-grid" x1="40" x2="252" y1="69.5" y2="69.5"/><text x="32" y="73.5" text-anchor="end">5</text><line class="ms-grid" x1="40" x2="252" y1="49.0" y2="49.0"/><text x="32" y="53.0" text-anchor="end">10</text><line class="ms-grid" x1="40" x2="252" y1="28.5" y2="28.5"/><text x="32" y="32.5" text-anchor="end">15</text><line class="ms-grid" x1="40" x2="252" y1="8.0" y2="8.0"/><text x="32" y="12.0" text-anchor="end">20</text><text x="75.3" y="110" text-anchor="middle">Mon</text><text x="146.0" y="110" text-anchor="middle">Tue</text><text x="216.7" y="110" text-anchor="middle">Wed</text><rect class="ms-s1" data-i="0" x="57.3" y="40.8" width="34.0" height="49.2" rx="2"/><rect class="ms-s1" data-i="1" x="128.0" y="16.2" width="34.0" height="73.8" rx="2"/><rect class="ms-s1" data-i="2" x="198.7" y="53.1" width="34.0" height="36.9" rx="2"/></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Sign-ups this week</caption><thead><tr><th scope="col">Day</th><th scope="col">Sign-ups</th></tr></thead><tbody><tr><th scope="row">Mon</th><td>12</td></tr><tr><th scope="row">Tue</th><td>18</td></tr><tr><th scope="row">Wed</th><td>9</td></tr></tbody></table></div></div>
</div></div></div><div class="tabs__panel" role="tabpanel" id="panel-2-1" aria-labelledby="tab-2-1" data-tab="Source"><figure class="cb" data-lang="markdown"><figcaption class="cb__head"><span class="cb__title">The model's answer</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t3">&lt;</span><span class="t1">Callout</span><span class="t2"> </span><span class="t1">tone</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">error</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">title</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Payment failed</span><span class="t3">&quot;</span><span class="t3">&gt;</span>
<span class="t2">The card was declined. Try another one.</span>
<span class="t3">&lt;/</span><span class="t1">Callout</span><span class="t3">&gt;</span><p><span class="t3">&lt;</span><span class="t1">Chart</span><span class="t2"> </span><span class="t1">type</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">column</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">title</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Sign-ups this week</span><span class="t3">&quot;</span><span class="t3">&gt;</span>
<span class="t2">| Day | Sign-ups |</span>
<span class="t2">|---|---|</span>
<span class="t2">| Mon | 12 |</span>
<span class="t2">| Tue | 18 |</span>
<span class="t2">| Wed | 9 |</span>
<span class="t3">&lt;/</span><span class="t1">Chart</span><span class="t3">&gt;</span></code></pre></figure></div></div></p>
<p>The core reads <code>error</code> as <code>danger</code> and <code>column</code> as <code>bar</code>, and notes both. The answer is still valid, so nothing goes back to the model.</p>
<h2 id="strict-where-it-isn-t"><a class="anchor" href="#strict-where-it-isn-t" aria-hidden="true" tabindex="-1">#</a>Strict where it isn&#39;t</h2>
<p>When the intent isn&#39;t clear, the line gets a diagnostic with a class, tied to the line it&#39;s on. A made-up <code>&lt;Weather city=&quot;Lisbon&quot;&gt;</code> is a <code>hallucinated-component</code>: it&#39;s sent for repair, and until then its words are drawn as plain text, so nothing the model said disappears.</p>
<div class="table-scroll" tabindex="0" role="region" aria-label="Table 1: Class, What it means"><table>
<thead>
<tr>
<th>Class</th>
<th>What it means</th>
</tr>
</thead>
<tbody><tr>
<td><code>hallucinated-component</code></td>
<td>a component that isn&#39;t in the app&#39;s catalog</td>
</tr>
<tr>
<td><code>signature-mismatch</code></td>
<td>an attribute the component doesn&#39;t have, or of the wrong kind</td>
</tr>
<tr>
<td><code>required-field</code></td>
<td>a required attribute left out</td>
</tr>
<tr>
<td><code>enum-mismatch</code></td>
<td>a value outside an attribute&#39;s list, with no default to fall back on</td>
</tr>
<tr>
<td><code>placement</code></td>
<td>content where its parent doesn&#39;t take it</td>
</tr>
<tr>
<td><code>malformed-syntax</code></td>
<td>a tag that doesn&#39;t close, or closes the wrong thing</td>
</tr>
<tr>
<td><code>unreadable-tag</code></td>
<td>a line that opens like a component but isn&#39;t a tag the core can read</td>
</tr>
<tr>
<td><code>unsafe-url</code></td>
<td>an address that could run code, like <code>javascript:</code></td>
</tr>
</tbody></table>
</div><p>None of what the model writes ever runs: no scripts, no inline styles, and pictures only from hosts the app allows.</p>
<h2 id="repair-only-where-it-s-needed"><a class="anchor" href="#repair-only-where-it-s-needed" aria-hidden="true" tabindex="-1">#</a>Repair, only where it&#39;s needed</h2>
<p>When a line breaks a rule, the API sends the model just the broken lines with what&#39;s wrong with each and asks for those lines again. Everything else stays as it streamed. Broken nesting is mended with no model at all, and the most common problem, an answer that left its tags open, gets its closing lines appended in place, so nothing on the page is swapped.</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>Valid <span class="ms-pair">answers, %</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="Valid answers, %" data-kind="horizontal" data-l="175" data-t="8" data-pw="509.0" data-ph="244.0" data-lo="0" data-hi="100" data-n="2"><line class="ms-grid" x1="175.0" x2="175.0" y1="8" y2="252"/><text x="175.0" y="270" text-anchor="middle">0</text><line class="ms-grid" x1="302.3" x2="302.3" y1="8" y2="252"/><text x="302.3" y="270" text-anchor="middle">25</text><line class="ms-grid" x1="429.5" x2="429.5" y1="8" y2="252"/><text x="429.5" y="270" text-anchor="middle">50</text><line class="ms-grid" x1="556.8" x2="556.8" y1="8" y2="252"/><text x="556.8" y="270" text-anchor="middle">75</text><line class="ms-grid" x1="684.0" x2="684.0" y1="8" y2="252"/><text x="684.0" y="270" text-anchor="middle">100</text><rect class="ms-s1" data-i="0" x="175.0" y="58.0" width="442.8" height="20.0" rx="2"/><text x="167" y="73.0" text-anchor="end">Before repair</text><rect class="ms-s1" data-i="1" x="175.0" y="180.0" width="509.0" height="20.0" rx="2"/><text x="167" y="195.0" text-anchor="end">After repair</text></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>Valid <span class="ms-pair">answers, %</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="Valid answers, %" data-kind="horizontal" data-l="103" data-t="8" data-pw="261.0" data-ph="129.0" data-lo="0" data-hi="100" data-n="2"><line class="ms-grid" x1="103.0" x2="103.0" y1="8" y2="137"/><text x="103.0" y="155" text-anchor="middle">0</text><line class="ms-grid" x1="168.3" x2="168.3" y1="8" y2="137"/><text x="168.3" y="155" text-anchor="middle">25</text><line class="ms-grid" x1="233.5" x2="233.5" y1="8" y2="137"/><text x="233.5" y="155" text-anchor="middle">50</text><line class="ms-grid" x1="298.8" x2="298.8" y1="8" y2="137"/><text x="298.8" y="155" text-anchor="middle">75</text><line class="ms-grid" x1="364.0" x2="364.0" y1="8" y2="137"/><text x="364.0" y="155" text-anchor="middle">100</text><rect class="ms-s1" data-i="0" x="103.0" y="29.3" width="227.1" height="20.0" rx="2"/><text x="95" y="37.3" text-anchor="end"><tspan x="95">Before</tspan><tspan x="95" dy="14">repair</tspan></text><rect class="ms-s1" data-i="1" x="103.0" y="93.8" width="261.0" height="20.0" rx="2"/><text x="95" y="108.8" text-anchor="end">After repair</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Valid <span class="ms-pair">answers, %</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Valid answers, %" data-kind="horizontal" data-l="103" data-t="8" data-pw="141.0" data-ph="86.0" data-lo="0" data-hi="100" data-n="2"><line class="ms-grid" x1="103.0" x2="103.0" y1="8" y2="94"/><text x="103.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="138.3" x2="138.3" y1="8" y2="94"/><text x="138.3" y="112" text-anchor="middle">25</text><line class="ms-grid" x1="173.5" x2="173.5" y1="8" y2="94"/><text x="173.5" y="112" text-anchor="middle">50</text><line class="ms-grid" x1="208.8" x2="208.8" y1="8" y2="94"/><text x="208.8" y="112" text-anchor="middle">75</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">100</text><rect class="ms-s1" data-i="0" x="103.0" y="18.5" width="122.7" height="20.0" rx="2"/><text x="95" y="26.5" text-anchor="end"><tspan x="95">Before</tspan><tspan x="95" dy="14">repair</tspan></text><rect class="ms-s1" data-i="1" x="103.0" y="61.5" width="141.0" height="20.0" rx="2"/><text x="95" y="76.5" text-anchor="end">After repair</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Valid answers, %</caption><thead><tr><th scope="col"></th><th scope="col">Valid</th></tr></thead><tbody><tr><th scope="row">Before repair</th><td>87.0</td></tr><tr><th scope="row">After repair</th><td>100</td></tr></tbody></table></div></div>
</div></div><figcaption>DeepSeek V4 Flash with reasoning off and every component on, the benchmark&#39;s 46 briefs four times each.</figcaption></figure><h2 id="testing-it"><a class="anchor" href="#testing-it" aria-hidden="true" tabindex="-1">#</a>Testing it</h2>
<p>Streaming bugs tend to live exactly at chunk boundaries. So every recorded answer we have, 3,371 of them, is fed to the core split at every possible point, and each split has to give the same tree as the whole answer.</p>
<p>A parser that reads untrusted text from a model should never panic either, so the core runs under a fuzzer for 24 hours at a time:</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><figure class="ms-timeline"><figcaption><span class="ms-nb">24-hour</span> fuzz runs</figcaption><ol><li><time>18 to 19 Oct</time><span><span class="ms-nb">187.3</span> million mutated answers and 3 panics from one bug, a chart nested so deep it got a negative width. Fixed, with a test.</span></li><li><time>19 to 20 Oct</time><span>One more panic at <span class="ms-nb">131.2</span> million, a code block whose first line began with a narrow no-break space. Fixed, with a test.</span></li><li><time>31 Oct to 1 Nov</time><span><span class="ms-nb">93.7</span> million, no failures.</span></li><li><time>3 to 4 Nov</time><span><span class="ms-nb">88.3</span> million, no failures.</span></li></ol></figure>
</div></div><figcaption>The 24-hour fuzz runs that found something, and the last two, on the core as it is now.</figcaption></figure><h2 id="try-it"><a class="anchor" href="#try-it" aria-hidden="true" tabindex="-1">#</a>Try it</h2>
<p>The web SDK is one package:</p>
<figure class="cb" data-lang="bash"><figcaption class="cb__head"><span class="cb__title">Terminal</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t1">npm</span><span class="t2"> </span><span class="t1">install</span><span class="t2"> </span><span class="t1">@morphscreen/web</span></code></pre></figure><p>Or start from a working React chat, with the server route and the page already wired:</p>
<figure class="cb" data-lang="bash"><figcaption class="cb__head"><span class="cb__title">Terminal</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t1">gh</span><span class="t2"> </span><span class="t1">repo</span><span class="t2"> </span><span class="t1">create</span><span class="t2"> </span><span class="t1">my-chat</span><span class="t2"> </span><span class="t1">--template</span><span class="t2"> </span><span class="t1">morphscreen/starter-react</span><span class="t2"> </span><span class="t1">--private</span><span class="t2"> </span><span class="t1">--clone</span></code></pre></figure><p>The core, the format and the SDKs are open source under Apache-2.0 at <a href="https://github.com/morphscreen/morphscreen">github.com/morphscreen/morphscreen</a>. <a href="https://morphscreen.com/docs/format/validation">Validation and repair</a> in the docs has every rule, and the <a href="https://morphscreen.com/benchmark">benchmark</a> has the numbers above with every raw answer.</p>
<p>If you&#39;re building something where a model answers with UI, I&#39;d like to hear what breaks for you.</p>
]]></description>
</item>
<item>
<title>We ran OpenUI&apos;s own benchmark. Here&apos;s where we lose.</title>
<link>https://morphscreen.com/blog/openui-benchmark-where-we-lose</link>
<guid isPermaLink="true">https://morphscreen.com/blog/openui-benchmark-where-we-lose</guid>
<pubDate>Fri, 06 Nov 2026 00:00:00 GMT</pubDate>
<description><![CDATA[<p>On OpenUI&#39;s own public benchmark, MorphScreen is ahead on 37 of 64 measures, even on 23 and behind on 4. All four losses are answer length: our answers run about 39% longer (1,268 tokens against 913). The biggest wins are small models (66.3% valid answers against 31.5% for Nemotron 3 Nano 30B), prompt size (2,954 tokens against 14,041) and accessibility (0 axe violations against 599).</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart ms-side"><figcaption><span>64 measures <span class="ms-pair">against OpenUI</span></span></figcaption><svg viewBox="0 0 240 240" role="img" aria-label="64 measures against OpenUI" data-kind="donut"><path class="ms-slice ms-s1" data-i="0" d="M120.0,8.0A112,112 0 1 1 67.2,218.8L88.3,179.3A67.2,67.2 0 1 0 120.0,52.8Z"/><path class="ms-slice ms-s2" data-i="1" d="M67.2,218.8A112,112 0 0 1 77.1,16.5L94.3,57.9A67.2,67.2 0 0 0 88.3,179.3Z"/><path class="ms-slice ms-s3" data-i="2" d="M77.1,16.5A112,112 0 0 1 120.0,8.0L120.0,52.8A67.2,67.2 0 0 0 94.3,57.9Z"/></svg><ul class="ms-legend"><li><i class="ms-s1"></i>MorphScreen ahead<span class="ms-v">58%</span></li><li><i class="ms-s2"></i>Even<span class="ms-v">36%</span></li><li><i class="ms-s3"></i>OpenUI ahead<span class="ms-v">6%</span></li></ul></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>64 measures <span class="ms-pair">against OpenUI</span></span></figcaption><svg viewBox="0 0 380 211" role="img" aria-label="64 measures against OpenUI" data-kind="donut"><path class="ms-slice ms-s1" data-i="0" d="M190.0,8.0A97.5,97.5 0 1 1 144.0,191.5L162.4,157.1A58.5,58.5 0 1 0 190.0,47.0Z"/><path class="ms-slice ms-s2" data-i="1" d="M144.0,191.5A97.5,97.5 0 0 1 152.7,15.4L167.6,51.5A58.5,58.5 0 0 0 162.4,157.1Z"/><path class="ms-slice ms-s3" data-i="2" d="M152.7,15.4A97.5,97.5 0 0 1 190.0,8.0L190.0,47.0A58.5,58.5 0 0 0 167.6,51.5Z"/></svg><ul class="ms-legend"><li><i class="ms-s1"></i>MorphScreen ahead<span class="ms-v">58%</span></li><li><i class="ms-s2"></i>Even<span class="ms-v">36%</span></li><li><i class="ms-s3"></i>OpenUI ahead<span class="ms-v">6%</span></li></ul></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>64 measures <span class="ms-pair">against OpenUI</span></span></figcaption><svg viewBox="0 0 260 157" role="img" aria-label="64 measures against OpenUI" data-kind="donut"><path class="ms-slice ms-s1" data-i="0" d="M130.0,8.0A70.5,70.5 0 1 1 96.8,140.7L110.1,115.8A42.3,42.3 0 1 0 130.0,36.2Z"/><path class="ms-slice ms-s2" data-i="1" d="M96.8,140.7A70.5,70.5 0 0 1 103.0,13.4L113.8,39.4A42.3,42.3 0 0 0 110.1,115.8Z"/><path class="ms-slice ms-s3" data-i="2" d="M103.0,13.4A70.5,70.5 0 0 1 130.0,8.0L130.0,36.2A42.3,42.3 0 0 0 113.8,39.4Z"/></svg><ul class="ms-legend"><li><i class="ms-s1"></i>MorphScreen ahead<span class="ms-v">58%</span></li><li><i class="ms-s2"></i>Even<span class="ms-v">36%</span></li><li><i class="ms-s3"></i>OpenUI ahead<span class="ms-v">6%</span></li></ul></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>64 measures against OpenUI</caption><thead><tr><th scope="col">Result</th><th scope="col">Measures</th></tr></thead><tbody><tr><th scope="row">MorphScreen ahead</th><td>37</td></tr><tr><th scope="row">Even</th><td>23</td></tr><tr><th scope="row">OpenUI ahead</th><td>4</td></tr></tbody></table></div></div>
</div></div><figcaption>64 measures, with the same briefs, models, providers and rules for both. Drawn by MorphScreen from the table in this post.</figcaption></figure><h2 id="why-their-benchmark"><a class="anchor" href="#why-their-benchmark" aria-hidden="true" tabindex="-1">#</a>Why their benchmark</h2>
<p>When you build something in a space that already has a known project, the obvious question is &quot;why not just use that?&quot; For generative UI, that project is OpenUI. So instead of writing my own benchmark, I ran theirs.</p>
<p>Thesys publishes generative-ui-bench for OpenUI. I used the same briefs, the same models and providers, and the same rules for what counts as valid, and ran MorphScreen next to the open-source OpenUI library. Everything is on <a href="https://morphscreen.com/benchmark">morphscreen.com/benchmark</a>: the method, every raw answer and every row.</p>
<p>One thing up front: this is the open-source library, not Thesys&#39;s hosted OpenUI Cloud. I haven&#39;t measured that, so nothing here says anything about it.</p>
<h2 id="how-a-row-counts"><a class="anchor" href="#how-a-row-counts" aria-hidden="true" tabindex="-1">#</a>How a row counts</h2>
<p>A lead only counts when it isn&#39;t chance. Where a row counts answers, a two-proportion test has to put the gap below p = 0.05, and timings within 10% of each other are even.</p>
<h2 id="what-each-format-looks-like"><a class="anchor" href="#what-each-format-looks-like" aria-hidden="true" tabindex="-1">#</a>What each format looks like</h2>
<p>The same brief, a newsletter sign-up card, answered by DeepSeek V4 Flash in each format. These are the raw answers from the benchmark, unedited.</p>
<figure class="cb" data-lang="markdown"><figcaption class="cb__head"><span class="cb__title">MorphScreen: Markdown with a few tags</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t3">&lt;</span><span class="t1">Card</span><span class="t2"> </span><span class="t1">title</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">The Week in Product</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">subtitle</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">One email, every Friday, with the best product thinking.</span><span class="t3">&quot;</span><span class="t3">&gt;</span>
<span class="t2">  </span><span class="t3">&lt;</span><span class="t1">Form</span><span class="t2"> </span><span class="t1">name</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">newsletter-signup</span><span class="t3">&quot;</span><span class="t3">&gt;</span>
<span class="t2">    </span><span class="t3">&lt;</span><span class="t1">Input</span><span class="t2"> </span><span class="t1">label</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">Email address</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">name</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">email</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">type</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">email</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">placeholder</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">you@example.com</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">required</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">true</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t1">hint</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">We'll never share your email</span><span class="t3">&quot;</span><span class="t2"> </span><span class="t3">/&gt;</span>
<span class="t2">    </span><span class="t3">&lt;</span><span class="t1">Button</span><span class="t3">&gt;</span><span class="t2">Subscribe</span><span class="t3">&lt;/</span><span class="t1">Button</span><span class="t3">&gt;</span>
<span class="t2">    </span><span class="t3">&lt;</span><span class="t1">Button</span><span class="t2"> </span><span class="t1">variant</span><span class="t3">=</span><span class="t3">&quot;</span><span class="t1">tertiary</span><span class="t3">&quot;</span><span class="t3">&gt;</span><span class="t2">No thanks</span><span class="t3">&lt;/</span><span class="t1">Button</span><span class="t3">&gt;</span>
<span class="t2">  </span><span class="t3">&lt;/</span><span class="t1">Form</span><span class="t3">&gt;</span>
<span class="t3">&lt;/</span><span class="t1">Card</span><span class="t3">&gt;</span></code></pre></figure><figure class="cb" data-lang="text"><figcaption class="cb__head"><span class="cb__title">OpenUI: OpenUI Lang</span><button type="button" class="cb__copy" data-copy>Copy</button></figcaption><pre tabindex="0"><code><span class="t2">root = Card([header, form])</span>
<span class="t2">header = CardHeader(&quot;The Weekly Build&quot;, &quot;One email every Friday — a short rundown of the best product, design, and engineering reads we found this week. No fluff, no spam, unsubscribe anytime.&quot;)</span>
<span class="t2">form = Form(&quot;newsletter-signup&quot;, formButtons, [emailField])</span>
<span class="t2">emailField = FormControl(&quot;Email address&quot;, emailInput, &quot;We'll only send you the newsletter, never anything else.&quot;)</span>
<span class="t2">emailInput = Input(&quot;email&quot;, &quot;you@example.com&quot;, &quot;email&quot;)</span>
<span class="t2">formButtons = Buttons([subscribeBtn])</span>
<span class="t2">subscribeBtn = Button(&quot;Subscribe&quot;, null, &quot;primary&quot;)</span></code></pre></figure><figure class="answer-figure"><div class="preview"><div class="ms-answer"><section class="ms-card"><header><h2>The Week <span class="ms-pair">in Product</span></h2><p>One email, every Friday, with the best product thinking.</p></header><form method="post" enctype="multipart/form-data" class="ms-form"><label class="ms-field"><span>Email address *</span><input name="email" type="email" placeholder="you@example.com" required><small>We'll never share your email</small></label>
<div class="ms-actions"><button class="ms-btn ms-primary">Subscribe</button><button class="ms-btn ms-tertiary">No thanks</button></div></form></section>
</div></div><figcaption>The MorphScreen answer above, drawn by MorphScreen&#39;s core.</figcaption></figure><p>OpenUI Lang names each piece and wires them together, so a component can be used before it&#39;s written. MorphScreen nests tags in the order they&#39;re drawn, so each line can go on the page as soon as it&#39;s finished. <a href="https://morphscreen.com/demo">The demo</a> draws both from the same answers side by side.</p>
<h2 id="where-we-lose"><a class="anchor" href="#where-we-lose" aria-hidden="true" tabindex="-1">#</a>Where we lose</h2>
<p>All four of OpenUI&#39;s wins are answer length. With reasoning off and every component on, our answers run longer. Markdown with tags is less compact than OpenUI Lang, which was designed to be short, and you pay for that in output tokens on every answer.</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>Answer <span class="ms-pair">length, tokens</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="Answer length, tokens" data-kind="horizontal" data-l="175" data-t="8" data-pw="509.0" data-ph="244.0" data-lo="0" data-hi="1500" data-n="2"><line class="ms-grid" x1="175.0" x2="175.0" y1="8" y2="252"/><text x="175.0" y="270" text-anchor="middle">0</text><line class="ms-grid" x1="344.7" x2="344.7" y1="8" y2="252"/><text x="344.7" y="270" text-anchor="middle">500</text><line class="ms-grid" x1="514.3" x2="514.3" y1="8" y2="252"/><text x="514.3" y="270" text-anchor="middle">1k</text><line class="ms-grid" x1="684.0" x2="684.0" y1="8" y2="252"/><text x="684.0" y="270" text-anchor="middle">1.5k</text><rect class="ms-s1" data-i="0" x="175.0" y="58.0" width="430.3" height="20.0" rx="2"/><text x="167" y="73.0" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="175.0" y="180.0" width="309.8" height="20.0" rx="2"/><text x="167" y="195.0" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>Answer <span class="ms-pair">length, tokens</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="Answer length, tokens" data-kind="horizontal" data-l="95" data-t="8" data-pw="269.0" data-ph="129.0" data-lo="0" data-hi="1500" data-n="2"><line class="ms-grid" x1="95.0" x2="95.0" y1="8" y2="137"/><text x="95.0" y="155" text-anchor="middle">0</text><line class="ms-grid" x1="184.7" x2="184.7" y1="8" y2="137"/><text x="184.7" y="155" text-anchor="middle">500</text><line class="ms-grid" x1="274.3" x2="274.3" y1="8" y2="137"/><text x="274.3" y="155" text-anchor="middle">1k</text><line class="ms-grid" x1="364.0" x2="364.0" y1="8" y2="137"/><text x="364.0" y="155" text-anchor="middle">1.5k</text><rect class="ms-s1" data-i="0" x="95.0" y="29.3" width="227.4" height="20.0" rx="2"/><text x="87" y="44.3" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="95.0" y="93.8" width="163.7" height="20.0" rx="2"/><text x="87" y="108.8" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Answer <span class="ms-pair">length, tokens</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Answer length, tokens" data-kind="horizontal" data-l="89" data-t="8" data-pw="155.0" data-ph="86.0" data-lo="0" data-hi="1500" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="94"/><text x="89.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="140.7" x2="140.7" y1="8" y2="94"/><text x="140.7" y="112" text-anchor="middle">500</text><line class="ms-grid" x1="192.3" x2="192.3" y1="8" y2="94"/><text x="192.3" y="112" text-anchor="middle">1k</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">1.5k</text><rect class="ms-s1" data-i="0" x="89.0" y="18.5" width="131.0" height="20.0" rx="2"/><text x="81" y="33.5" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="61.5" width="94.3" height="20.0" rx="2"/><text x="81" y="76.5" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Answer length, tokens</caption><thead><tr><th scope="col"></th><th scope="col">Tokens</th></tr></thead><tbody><tr><th scope="row">MorphScreen</th><td>1268</td></tr><tr><th scope="row">OpenUI</th><td>913</td></tr></tbody></table></div></div>
</div></div><figcaption>Output tokens per answer, DeepSeek V4 Flash with reasoning off and every component on. Lower is better.</figcaption></figure><h2 id="where-it-s-even"><a class="anchor" href="#where-it-s-even" aria-hidden="true" tabindex="-1">#</a>Where it&#39;s even</h2>
<p>Validity before repair, on the default setup (DeepSeek V4 Flash, reasoning off, every component): 87.0% each. On a capable model, both formats come out right most of the time.</p>
<h2 id="where-we-re-ahead"><a class="anchor" href="#where-we-re-ahead" aria-hidden="true" tabindex="-1">#</a>Where we&#39;re ahead</h2>
<p>Small models. Nemotron 3 Nano 30B wrote a valid MorphScreen answer 66.3% of the time and a valid OpenUI one 31.5%, on the 46 briefs twice each. Models already know Markdown, so there&#39;s less to learn.</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>Valid answers from a 30B <span class="ms-pair">model, %</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="Valid answers from a 30B model, %" data-kind="horizontal" data-l="175" data-t="8" data-pw="509.0" data-ph="244.0" data-lo="0" data-hi="80" data-n="2"><line class="ms-grid" x1="175.0" x2="175.0" y1="8" y2="252"/><text x="175.0" y="270" text-anchor="middle">0</text><line class="ms-grid" x1="302.3" x2="302.3" y1="8" y2="252"/><text x="302.3" y="270" text-anchor="middle">20</text><line class="ms-grid" x1="429.5" x2="429.5" y1="8" y2="252"/><text x="429.5" y="270" text-anchor="middle">40</text><line class="ms-grid" x1="556.8" x2="556.8" y1="8" y2="252"/><text x="556.8" y="270" text-anchor="middle">60</text><line class="ms-grid" x1="684.0" x2="684.0" y1="8" y2="252"/><text x="684.0" y="270" text-anchor="middle">80</text><rect class="ms-s1" data-i="0" x="175.0" y="58.0" width="421.8" height="20.0" rx="2"/><text x="167" y="73.0" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="175.0" y="180.0" width="200.4" height="20.0" rx="2"/><text x="167" y="195.0" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>Valid answers from a 30B <span class="ms-pair">model, %</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="Valid answers from a 30B model, %" data-kind="horizontal" data-l="95" data-t="8" data-pw="269.0" data-ph="129.0" data-lo="0" data-hi="80" data-n="2"><line class="ms-grid" x1="95.0" x2="95.0" y1="8" y2="137"/><text x="95.0" y="155" text-anchor="middle">0</text><line class="ms-grid" x1="162.3" x2="162.3" y1="8" y2="137"/><text x="162.3" y="155" text-anchor="middle">20</text><line class="ms-grid" x1="229.5" x2="229.5" y1="8" y2="137"/><text x="229.5" y="155" text-anchor="middle">40</text><line class="ms-grid" x1="296.8" x2="296.8" y1="8" y2="137"/><text x="296.8" y="155" text-anchor="middle">60</text><line class="ms-grid" x1="364.0" x2="364.0" y1="8" y2="137"/><text x="364.0" y="155" text-anchor="middle">80</text><rect class="ms-s1" data-i="0" x="95.0" y="29.3" width="222.9" height="20.0" rx="2"/><text x="87" y="44.3" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="95.0" y="93.8" width="105.9" height="20.0" rx="2"/><text x="87" y="108.8" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Valid answers from a 30B <span class="ms-pair">model, %</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Valid answers from a 30B model, %" data-kind="horizontal" data-l="89" data-t="8" data-pw="155.0" data-ph="86.0" data-lo="0" data-hi="80" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="94"/><text x="89.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="127.8" x2="127.8" y1="8" y2="94"/><text x="127.8" y="112" text-anchor="middle">20</text><line class="ms-grid" x1="166.5" x2="166.5" y1="8" y2="94"/><text x="166.5" y="112" text-anchor="middle">40</text><line class="ms-grid" x1="205.3" x2="205.3" y1="8" y2="94"/><text x="205.3" y="112" text-anchor="middle">60</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">80</text><rect class="ms-s1" data-i="0" x="89.0" y="18.5" width="128.5" height="20.0" rx="2"/><text x="81" y="33.5" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="61.5" width="61.0" height="20.0" rx="2"/><text x="81" y="76.5" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Valid answers from a 30B model, %</caption><thead><tr><th scope="col"></th><th scope="col">Valid</th></tr></thead><tbody><tr><th scope="row">MorphScreen</th><td>66.3</td></tr><tr><th scope="row">OpenUI</th><td>31.5</td></tr></tbody></table></div></div>
</div></div><figcaption>Nemotron 3 Nano 30B on the benchmark&#39;s 46 briefs, twice each. Higher is better.</figcaption></figure><p>The prompt. With every component on both sides (our 75, the 63 in OpenUI&#39;s chat library), ours is a fifth the size. It goes with every call, so this is a cost on every answer.</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-chart-fit"><div class="ms-c-hi"><figure class="ms-chart"><figcaption><span>System <span class="ms-pair">prompt, tokens</span></span></figcaption><svg viewBox="0 0 700 276" role="img" aria-label="System prompt, tokens" data-kind="horizontal" data-l="175" data-t="8" data-pw="509.0" data-ph="244.0" data-lo="0" data-hi="15000" data-n="2"><line class="ms-grid" x1="175.0" x2="175.0" y1="8" y2="252"/><text x="175.0" y="270" text-anchor="middle">0</text><line class="ms-grid" x1="344.7" x2="344.7" y1="8" y2="252"/><text x="344.7" y="270" text-anchor="middle">5k</text><line class="ms-grid" x1="514.3" x2="514.3" y1="8" y2="252"/><text x="514.3" y="270" text-anchor="middle">10k</text><line class="ms-grid" x1="684.0" x2="684.0" y1="8" y2="252"/><text x="684.0" y="270" text-anchor="middle">15k</text><rect class="ms-s1" data-i="0" x="175.0" y="58.0" width="100.2" height="20.0" rx="2"/><text x="167" y="73.0" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="175.0" y="180.0" width="476.5" height="20.0" rx="2"/><text x="167" y="195.0" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-mid"><figure class="ms-chart"><figcaption><span>System <span class="ms-pair">prompt, tokens</span></span></figcaption><svg viewBox="0 0 380 161" role="img" aria-label="System prompt, tokens" data-kind="horizontal" data-l="95" data-t="8" data-pw="269.0" data-ph="129.0" data-lo="0" data-hi="15000" data-n="2"><line class="ms-grid" x1="95.0" x2="95.0" y1="8" y2="137"/><text x="95.0" y="155" text-anchor="middle">0</text><line class="ms-grid" x1="184.7" x2="184.7" y1="8" y2="137"/><text x="184.7" y="155" text-anchor="middle">5k</text><line class="ms-grid" x1="274.3" x2="274.3" y1="8" y2="137"/><text x="274.3" y="155" text-anchor="middle">10k</text><line class="ms-grid" x1="364.0" x2="364.0" y1="8" y2="137"/><text x="364.0" y="155" text-anchor="middle">15k</text><rect class="ms-s1" data-i="0" x="95.0" y="29.3" width="53.0" height="20.0" rx="2"/><text x="87" y="44.3" text-anchor="end">MorphScreen</text><rect class="ms-s1" data-i="1" x="95.0" y="93.8" width="251.8" height="20.0" rx="2"/><text x="87" y="108.8" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>System <span class="ms-pair">prompt, tokens</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="System prompt, tokens" data-kind="horizontal" data-l="89" data-t="8" data-pw="155.0" data-ph="86.0" data-lo="0" data-hi="15000" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="94"/><text x="89.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="140.7" x2="140.7" y1="8" y2="94"/><text x="140.7" y="112" text-anchor="middle">5k</text><line class="ms-grid" x1="192.3" x2="192.3" y1="8" y2="94"/><text x="192.3" y="112" text-anchor="middle">10k</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">15k</text><rect class="ms-s1" data-i="0" x="89.0" y="18.5" width="30.5" height="20.0" rx="2"/><text x="81" y="33.5" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="61.5" width="145.1" height="20.0" rx="2"/><text x="81" y="76.5" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>System prompt, tokens</caption><thead><tr><th scope="col"></th><th scope="col">Tokens</th></tr></thead><tbody><tr><th scope="row">MorphScreen</th><td>2954</td></tr><tr><th scope="row">OpenUI</th><td>14041</td></tr></tbody></table></div></div>
</div></div><figcaption>The system prompt with every component, in tokens. Lower is better.</figcaption></figure><p>After repair, 100% valid (184 of 184) against 84.8%. OpenUI&#39;s open-source SDK has no repair step, so this row is really &quot;with repair against without&quot;.</p>
<p>Accessibility: 0 axe violations across 20 answers in Chrome, against 599. And in the browser, less work:</p>
<figure class="answer-figure"><div class="preview"><div class="ms-answer"><div class="ms-cols ms-n2 ms-m280 ms-rich"><div class="ms-cell"><div class="ms-chart-fit"><div class="ms-c-hi ms-c-mid"><figure class="ms-chart"><figcaption><span>CPU to stream and <span class="ms-pair">draw, ms</span></span></figcaption><svg viewBox="0 0 312 136" role="img" aria-label="CPU to stream and draw, ms" data-kind="horizontal" data-l="89" data-t="8" data-pw="207.0" data-ph="104.0" data-lo="0" data-hi="6000" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="112"/><text x="89.0" y="130" text-anchor="middle">0</text><line class="ms-grid" x1="158.0" x2="158.0" y1="8" y2="112"/><text x="158.0" y="130" text-anchor="middle">2k</text><line class="ms-grid" x1="227.0" x2="227.0" y1="8" y2="112"/><text x="227.0" y="130" text-anchor="middle">4k</text><line class="ms-grid" x1="296.0" x2="296.0" y1="8" y2="112"/><text x="296.0" y="130" text-anchor="middle">6k</text><rect class="ms-s1" data-i="0" x="89.0" y="23.0" width="36.3" height="20.0" rx="2"/><text x="81" y="38.0" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="75.0" width="192.0" height="20.0" rx="2"/><text x="81" y="90.0" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>CPU to stream and <span class="ms-pair">draw, ms</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="CPU to stream and draw, ms" data-kind="horizontal" data-l="89" data-t="8" data-pw="155.0" data-ph="86.0" data-lo="0" data-hi="6000" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="94"/><text x="89.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="140.7" x2="140.7" y1="8" y2="94"/><text x="140.7" y="112" text-anchor="middle">2k</text><line class="ms-grid" x1="192.3" x2="192.3" y1="8" y2="94"/><text x="192.3" y="112" text-anchor="middle">4k</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">6k</text><rect class="ms-s1" data-i="0" x="89.0" y="18.5" width="27.2" height="20.0" rx="2"/><text x="81" y="33.5" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="61.5" width="143.8" height="20.0" rx="2"/><text x="81" y="76.5" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>CPU to stream and draw, ms</caption><thead><tr><th scope="col"></th><th scope="col">ms</th></tr></thead><tbody><tr><th scope="row">MorphScreen</th><td>1052</td></tr><tr><th scope="row">OpenUI</th><td>5566</td></tr></tbody></table></div></div></div><div class="ms-cell"><div class="ms-chart-fit"><div class="ms-c-hi ms-c-mid"><figure class="ms-chart"><figcaption><span>Time to first <span class="ms-pair">text, ms</span></span></figcaption><svg viewBox="0 0 312 136" role="img" aria-label="Time to first text, ms" data-kind="horizontal" data-l="89" data-t="8" data-pw="207.0" data-ph="104.0" data-lo="0" data-hi="150" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="112"/><text x="89.0" y="130" text-anchor="middle">0</text><line class="ms-grid" x1="158.0" x2="158.0" y1="8" y2="112"/><text x="158.0" y="130" text-anchor="middle">50</text><line class="ms-grid" x1="227.0" x2="227.0" y1="8" y2="112"/><text x="227.0" y="130" text-anchor="middle">100</text><line class="ms-grid" x1="296.0" x2="296.0" y1="8" y2="112"/><text x="296.0" y="130" text-anchor="middle">150</text><rect class="ms-s1" data-i="0" x="89.0" y="23.0" width="98.0" height="20.0" rx="2"/><text x="81" y="38.0" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="75.0" width="190.4" height="20.0" rx="2"/><text x="81" y="90.0" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-c-lo"><figure class="ms-chart"><figcaption><span>Time to first <span class="ms-pair">text, ms</span></span></figcaption><svg viewBox="0 0 260 118" role="img" aria-label="Time to first text, ms" data-kind="horizontal" data-l="89" data-t="8" data-pw="155.0" data-ph="86.0" data-lo="0" data-hi="150" data-n="2"><line class="ms-grid" x1="89.0" x2="89.0" y1="8" y2="94"/><text x="89.0" y="112" text-anchor="middle">0</text><line class="ms-grid" x1="140.7" x2="140.7" y1="8" y2="94"/><text x="140.7" y="112" text-anchor="middle">50</text><line class="ms-grid" x1="192.3" x2="192.3" y1="8" y2="94"/><text x="192.3" y="112" text-anchor="middle">100</text><line class="ms-grid" x1="244.0" x2="244.0" y1="8" y2="94"/><text x="244.0" y="112" text-anchor="middle">150</text><rect class="ms-s1" data-i="0" x="89.0" y="18.5" width="73.4" height="20.0" rx="2"/><text x="81" y="33.5" text-anchor="end">MorphScre…</text><rect class="ms-s1" data-i="1" x="89.0" y="61.5" width="142.6" height="20.0" rx="2"/><text x="81" y="76.5" text-anchor="end">OpenUI</text></svg></figure></div><div class="ms-sr"><table class="ms-chart-data"><caption>Time to first text, ms</caption><thead><tr><th scope="col"></th><th scope="col">ms</th></tr></thead><tbody><tr><th scope="row">MorphScreen</th><td>71</td></tr><tr><th scope="row">OpenUI</th><td>138</td></tr></tbody></table></div></div></div></div>
</div></div><figcaption>20 answers streamed and drawn in Chrome. Lower is better on both.</figcaption></figure><h2 id="which-to-pick"><a class="anchor" href="#which-to-pick" aria-hidden="true" tabindex="-1">#</a>Which to pick</h2>
<p>If your answers are short and your model is big, the difference is small and OpenUI is a fine choice, especially if you want everything self-hosted. MorphScreen pulls ahead with small or cheap models, long prompts with many components, and pages that have to pass an accessibility check. <a href="https://morphscreen.com/docs/comparison">Generative UI SDKs compared</a> puts both next to CopilotKit, the Vercel AI SDK, A2UI and others.</p>
<h2 id="check-it-yourself"><a class="anchor" href="#check-it-yourself" aria-hidden="true" tabindex="-1">#</a>Check it yourself</h2>
<p>The benchmark code and every raw answer are in <a href="https://github.com/morphscreen/morphscreen">the repository</a>, and the <a href="https://morphscreen.com/benchmark">benchmark page</a> has each row with its method. If you think a row is measured unfairly, open an issue and I&#39;ll rerun it.</p>
]]></description>
</item>
</channel>
</rss>
