<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xml.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>iammatthias — #user-interface</title><description>Entries tagged user-interface.</description><link>https://iammatthias.com/</link><language>en-us</language><item><title>Post-UI</title><link>https://iammatthias.com/posts/1784126858578-post-ui/</link><guid isPermaLink="true">https://iammatthias.com/posts/1784126858578-post-ui/</guid><description>Do we still need to ship a frontend?</description><pubDate>Wed, 15 Jul 2026 17:27:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been thinking a lot about “post-UI”, whatever that means. It&amp;#39;s been rattling around in my head for months, &amp;quot;do we even need to ship a frontend?&amp;quot;. If the client can understand the shape of a response and the response has enough context, why can&amp;#39;t it build its own interface?&lt;/p&gt;
&lt;p&gt;The idea isn’t new. HATEOAS has been around since the early days. A response could details how to move through an application alongside the current state, letting the client discover the system as you move through it. And I think we&amp;#39;re on the cusp of this becoming reality, as local models grow in capability while shrinking in size.&lt;/p&gt;
&lt;h2&gt;The first build&lt;/h2&gt;
&lt;p&gt;The first attempt was garbage. I can&amp;#39;t even really call it mine. With all the fuss around Fable and Codex oneshotting tasks I gave each LLM a rough PRD, and let them run. Got a lot of code that looked vaguely related to the prompt, including one version that had prebuilt layouts wrapped around prettified JSON, which... no. Missed the mark and burned my usage limits.&lt;/p&gt;
&lt;p&gt;Archived those two and simplified to a Vite SPA with a &lt;code&gt;hello world&lt;/code&gt;, and soon switched to Svelte while the idea was still small enough to hold in my head.&lt;/p&gt;
&lt;h2&gt;The shape&lt;/h2&gt;
&lt;p&gt;From there I started replacing assumptions with a contract.&lt;/p&gt;
&lt;p&gt;The host fetches a JSON object and reduces it to a typed silhouette. Keys and nesting stay intact. Arrays use the shape of their first item, so a week of forecast data costs about the same prompt space as a single day.&lt;/p&gt;
&lt;p&gt;An abbreviated weather response becomes this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;{
  latitude:n,
  longitude:n,
  current:{time:s,temperature_2m:n,weather_code:n},
  daily:{time:[s],temperature_2m_max:[n],temperature_2m_min:[n]}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model gets the silhouette, route context, a little steering, and some libraries it can use. It does not get Los Angeles, the current temperature, the forecast, or anything else.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreiewg2wgrwiedu2agiuc76ttuj2sftzvpmv6hu5eb5v7wrfkrms37y&amp;amp;w=960&amp;amp;q=80&amp;amp;output=webp&quot; alt=&quot;The exact weather JSON beside the interface generated from its typed shape.&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;The runtime response stays on the left. The model received only its typed silhouette.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I started with a Shadow DOM. Fine for CSS, but not for JavaScript. So I switched to an &lt;code&gt;iframe&lt;/code&gt; . The host builds the document and gives the generated code &lt;code&gt;root&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, and explicitly allowed libraries. Imports, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, dynamic loading, nested frames, and external styles are rejected by the contract unless explicitly allowed.&lt;/p&gt;
&lt;p&gt;There is still plenty of authored client code wrapping everything. Svelte fetches data, passes the shape to an LLM, handles routing, caching, and the iframe. It just doesn&amp;#39;t know what weather, a Pokémon, or an earthquake page should look at first.&lt;/p&gt;
&lt;h2&gt;Testing it&lt;/h2&gt;
&lt;p&gt;Once weather worked, I threw a few public APIs at it. Hacker News, an OpenAPI document, earthquake data, and PokéAPI. Different shapes, different amounts of nesting, and no agreement about what an interface should look like.&lt;/p&gt;
&lt;p&gt;Some tests got steering. The earthquake prompt asks for an interactive globe and gives the model Three.js. That is enough to aim the model without hiding a globe component in the prototype.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreieqkhyp7gioinrwv66ig5ypaqhgwmuexdajueub5hqylt3drypr5q&amp;amp;w=960&amp;amp;q=80&amp;amp;output=webp&quot; alt=&quot;The USGS earthquake response beside the Three.js interface generated around it.&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;The response provides the events. The steering supplies a direction, not a globe component.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Walking an API&lt;/h2&gt;
&lt;p&gt;PokéAPI was the first test where a link mattered. Activating a relation in the collection renderer makes the iframe send an intent back to the host. The host fetches the next object, infers the shape, then generates or reuses another renderer.&lt;/p&gt;
&lt;p&gt;Collection, creature, and species are three different systems discovered while moving through the API. This is the part I was actually trying to get to. The response carries the next move alongside the current state, and the client discovers another system by following it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreifqx7m3krrupzzzvhyo7uapfuq42asomvuqfhmghptneg3dsqmeru&amp;amp;w=960&amp;amp;q=80&amp;amp;output=webp&quot; alt=&quot;The exact collection, creature, and species responses beside their generated Pokédex systems.&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Each navigation changes both the response and the generated system: collection, creature, then species.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The wait&lt;/h2&gt;
&lt;p&gt;Once the renderers worked, I timed them. A run ended when the iframe had a document, reported that it was no longer busy, and lost the loading overlay. The model stayed fixed at &lt;code&gt;gpt-5.6-sol&lt;/code&gt;, reasoning &lt;code&gt;none&lt;/code&gt;, on the &lt;code&gt;fast&lt;/code&gt; service tier.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Runtime JSON&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Data&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Generation&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Cold total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Weather&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1.6 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.8 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;42.8 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;44.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hacker News&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;8.4 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.4 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;26.6 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;27.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3.2 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.6 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;30.9 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;32.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Art Institute&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;6.4 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.2 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;31.6 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;32.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earthquakes + Three.js&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;200.6 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.2 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;55.3 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;56.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pokédex cold stages&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;28.9 KB&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;0.5 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;116.2 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;118.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Those are selected results, with three cold runs behind each row. Enough to find the bottleneck, not enough to pretend this is a population-level benchmark. Across the full 27-run set, navigation to ready averaged 38.7 seconds, which is pretty abysmal.&lt;/p&gt;
&lt;h2&gt;Kimi&lt;/h2&gt;
&lt;p&gt;To check whether I&amp;#39;d tuned the contract around Sol, I sent the same first-pass prompts to &lt;code&gt;moonshotai/kimi-k3&lt;/code&gt; through OpenRouter. The application&amp;#39;s first-pass prompt hashes matched for all nine shapes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Ready&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Ready-time figure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Sol&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;27 / 27&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;38.7 s across all 27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K3&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;3 / 11&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;219.5 s for the 3 successes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The Kimi number is the flattering version because it only includes the three renderers that finished. Their matching Sol samples averaged 32.7 seconds. Successful Kimi runs were 6.7 times slower before counting the other eight attempts that never produced anything usable.&lt;/p&gt;
&lt;p&gt;When it got through, the work was good. Kimi turned the art response into a quiet collection wall and the 151 Pokémon into a searchable field guide.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreifktpo3qj2akcjav7okyvdecfevzqdhsgwlshdz5d6gmy5cb3eema&amp;amp;w=960&amp;amp;q=80&amp;amp;output=webp&quot; alt=&quot;The exact Art Institute response beside the collection wall generated by Kimi K3.&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;This is one of the Kimi requests that made it through. The values stayed browser-side here too.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The 11 browser attempts made 61 OpenRouter HTTP attempts. Some died on repeated &lt;code&gt;429&lt;/code&gt;s in about 34 seconds. Others sat pending until the 235-second request deadline and produced no usable renderer. Species got one after 207.8 seconds, failed a static-style check, and entered repair. The browser&amp;#39;s 240-second cutoff got there first.&lt;/p&gt;
&lt;p&gt;The responses logged at least $0.409 in cost, including that unusable species renderer. Calls that failed without usage data may have added more. No second same-model endpoint was visible. Maybe that was temporary capacity trouble. It was still the path the browser had to deal with.&lt;/p&gt;
&lt;p&gt;I went into that pass thinking generation speed was the bottleneck. Kimi split it into admission, generation, and repair. Completion rate belongs next to latency or the number is mostly fiction.&lt;/p&gt;
&lt;h2&gt;Local&lt;/h2&gt;
&lt;p&gt;I want this to be local. In my head, that is the end goal, the ultimate realization of this idea, the thing that just isn&amp;#39;t possible yet. Local models are progressing with leaps and bounds and they still fall flat on their face when you task them with anything complex. And this leads to another assumption I&amp;#39;m making — specialized local models with distinct training sets are going to reach the OS level (hi radiant.computer), handling background tasks like rendering and file management. 7mb embedding models like &lt;a href=&quot;https://www.npmjs.com/package/@ternlight/base&quot;&gt;Ternlight&lt;/a&gt; are already available, and then there is stuff like Ternary Bonsai, which can run comfortably on webgpu with Transformers.js&lt;/p&gt;
&lt;p&gt;I tried a few quantized Gemma models and some of the smaller Ternary Bonsai models first. The ones that fit could begin the job. Finishing it was another matter.&lt;/p&gt;
&lt;p&gt;Bonsai 1.7B was the first one that &amp;quot;worked&amp;quot;. Kinda. Not really. Asking for a complete renderer in one go was too much, so I had to break it into a few smaller tasks. That was enough to get a rough page.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreicerqivfesreqozr23kmnctka7ny6maaosa225wo457jn2we6c2mm&amp;amp;w=960&amp;amp;q=80&amp;amp;output=webp&quot; alt=&quot;The browser-status JSON beside the interface generated locally by Bonsai 1.7B.&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;Same shape-to-renderer path, this time without a remote model.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I tried that same page with Transformers.js and &lt;a href=&quot;https://github.com/stfurkan/bitgpu&quot;&gt;BitGPU&lt;/a&gt;. Two runs each, hardly science, but enough to get a feel for where the wait was coming from.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;First token&lt;/th&gt;
&lt;th align=&quot;right&quot;&gt;Full page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Transformers.js&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;2.6–3.9 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;34.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BitGPU&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;1.0–1.1 s&lt;/td&gt;
&lt;td align=&quot;right&quot;&gt;50.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;BitGPU got to the first token about two seconds sooner, then finished the page about fifteen seconds later. I care about when the page exists, so Transformers.js won this very small test.&lt;/p&gt;
&lt;p&gt;The weird part is that I expected the model to be the failure. Bonsai understood the contract well enough to make a rough browser-status page from the silhouette. Waiting was worse. Thirty-four seconds warm is bad. I cleared the browser cache and got 73.4 seconds, which I only tolerate because I built the thing.&lt;/p&gt;
&lt;p&gt;I think the mistake might be asking a general model to do this. Rendering a page from one of these shapes is a narrow job, and the contract is already doing a lot of the work. A small model trained specifically for this shape-to-renderer loop would have less to figure out each time. I don&amp;#39;t have that model, but Bonsai got close enough to make me want one.&lt;/p&gt;
&lt;p&gt;The remote models are staying for now because waiting half a minute makes every contract change miserable to test. They let me keep working on the shape, walking APIs, and steering while the local path sits beside them. I mostly keep refreshing it to see how much of that minute I can remove.&lt;/p&gt;
</content:encoded><media:content url="https://wsrv.nl/?url=https%3A%2F%2Fblobs.farfield.systems%2Fblobs%2Fbafkreiewg2wgrwiedu2agiuc76ttuj2sftzvpmv6hu5eb5v7wrfkrms37y&amp;w=960&amp;q=80&amp;output=webp" medium="image"/><category>Posts</category><category>web-development</category><category>user-interface</category><category>hateoas</category><category>llm</category><category>webgpu</category><category>browser</category></item></channel></rss>