<?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>DEV Community: Anton Shubin</title>
    <description>The latest articles on DEV Community by Anton Shubin (@anton_shubin_f26b3cb5a572).</description>
    <link>https://dev.to/anton_shubin_f26b3cb5a572</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3723221%2F6e1f3495-1f01-437b-818f-34863c61017a.webp</url>
      <title>DEV Community: Anton Shubin</title>
      <link>https://dev.to/anton_shubin_f26b3cb5a572</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anton_shubin_f26b3cb5a572"/>
    <language>en</language>
    <item>
      <title>preact-components: a Preact component library without shadcn or Radix</title>
      <dc:creator>Anton Shubin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:42:52 +0000</pubDate>
      <link>https://dev.to/anton_shubin_f26b3cb5a572/preact-components-a-preact-component-library-without-shadcn-or-radix-1n8p</link>
      <guid>https://dev.to/anton_shubin_f26b3cb5a572/preact-components-a-preact-component-library-without-shadcn-or-radix-1n8p</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preact-components is my open-source Preact component library: ten JSR packages, rendered on the server, with roles, keyboard handling and focus written by hand.&lt;/li&gt;
&lt;li&gt;I own my components instead of using shadcn, Radix or MUI, because working around their styling, upgrades and limits cost me more than owning them.&lt;/li&gt;
&lt;li&gt;Owning components means owning accessibility, so headless Chromium checks the keyboard handling on every pull request.&lt;/li&gt;
&lt;li&gt;It is beta: no screen reader has been run against it, only Deno is tested, and it is Preact and Tailwind only. Try any component in the live guide first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;preact-components&lt;/strong&gt;: Live: &lt;a href="https://spy4x.github.io/preact-components" rel="noopener noreferrer"&gt;Open the live UI guide&lt;/a&gt; · Code: &lt;a href="https://github.com/spy4x/preact-components" rel="noopener noreferrer"&gt;github.com/spy4x/preact-components&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fdashboard-hero.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fdashboard-hero.webp" title="Every part of this demo dashboard is a component from the packages." alt="A dashboard in the live guide built only from the library: a frame titled Orbit with Run checks and New project buttons, a filter column, four KPI tiles, a line chart of passed and failed builds this week, and a sortable table of five projects with status badges" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every new app I start needs the same things: a button, a table, a dialog, a form&lt;br&gt;
field, a chart, a signed-in layout. I kept rebuilding them, or pulling in a&lt;br&gt;
component library and then working around it.&lt;/p&gt;

&lt;p&gt;preact-components is where that stopped. It is an open-source Preact component&lt;br&gt;
library: ten packages on JSR, every component rendered on the server and&lt;br&gt;
hydrated in the browser, with roles, labels, keyboard handling and focus written&lt;br&gt;
by hand. No shadcn, Radix, Headless UI or Material underneath.&lt;/p&gt;

&lt;p&gt;This post is for developers who build with Preact and Tailwind and wonder&lt;br&gt;
whether owning the UI layer is worth it. I'll show why I decided it is, what is&lt;br&gt;
in the ten packages, how to try it in a minute, and what is not done yet.&lt;/p&gt;

&lt;p&gt;The dashboard above is on the guide's first page. Every part of it is a&lt;br&gt;
component from these packages, running on local state: you can filter and sort&lt;br&gt;
the projects, run the checks for a toast, and add a project in the dialog.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not shadcn, Radix or MUI?
&lt;/h2&gt;

&lt;p&gt;Two of my earlier apps used shadcn, Radix and bits-ui. Over time those libraries&lt;br&gt;
cost me more to work around than it would have cost to own the components, in&lt;br&gt;
three ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Styling opinions.&lt;/strong&gt; A library ships a look. I already had design tokens.
Either I fight its defaults on every component, or I slowly give up my
tokens. The second one wins, quietly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade breakage.&lt;/strong&gt; A component library changes its API and its rendered
markup. Every upgrade reopens components that already worked, for no product
reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extension limits.&lt;/strong&gt; The component I need is never quite the one it ships.
The workaround is often more code than the component, and it inherits the
library's constraints on top.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the repository has a written rule: every component is written there. No&lt;br&gt;
third-party component library is a dependency, and a copy of one pasted into the&lt;br&gt;
tree counts as one too. The old apps are still useful, but only as a reference&lt;br&gt;
for design intent: I read how they compose a dialog and write my own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Port the markup and the behaviour. Never the dependency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That doesn't mean I write everything. My rule is "own the small, keep the huge".&lt;br&gt;
Preact, signals, Tailwind, arktype, wouter and Leaflet stay: they are large,&lt;br&gt;
well-solved problems, and none of them is a component library. A button, a&lt;br&gt;
dialog or a combobox is small enough to own, and opinionated enough that I'd&lt;br&gt;
fight someone else's defaults.&lt;/p&gt;
&lt;h2&gt;
  
  
  The real price: accessibility
&lt;/h2&gt;

&lt;p&gt;Owning components means owning everything a library used to hide. The repository&lt;br&gt;
names it plainly, so nobody pretends it is free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;focus traps: moving focus into a dialog, keeping it there, returning it on
close;&lt;/li&gt;
&lt;li&gt;dismissal: an outside click, &lt;code&gt;Escape&lt;/code&gt;, and the difference between dismissing
and closing on purpose;&lt;/li&gt;
&lt;li&gt;ARIA wiring: which &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;aria-expanded&lt;/code&gt; or &lt;code&gt;aria-labelledby&lt;/code&gt; goes on which
element;&lt;/li&gt;
&lt;li&gt;keyboard navigation: arrow keys, &lt;code&gt;Home&lt;/code&gt; and &lt;code&gt;End&lt;/code&gt;, typeahead;&lt;/li&gt;
&lt;li&gt;positioning: anchoring a popup to its trigger and flipping it when it would
overflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fdialog-focus.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fdialog-focus.webp" title="The demo's New project dialog, a native dialog element: focus moves into its field when it opens." alt="The New project dialog open over the guide's demo dashboard: a Project name field with a focus ring, the hint " width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where the platform already has an answer, I use it: native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; and forms that post before any script has run. A native&lt;br&gt;
element ships its semantics for free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fcombobox-keyboard.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fcombobox-keyboard.webp" title="The Combobox, driven from the keyboard in the live guide." alt="The Combobox card in the live guide: the city field has focus and its list is open, and the arrow keys have highlighted São Paulo" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Combobox above is a good example. Every one renders a single &lt;code&gt;role="status"&lt;/code&gt;&lt;br&gt;
live region, already in the server HTML and empty until the field is used, so&lt;br&gt;
the answers it announces have a stable place to go.&lt;/p&gt;

&lt;p&gt;How do I know the keyboard handling works? The packages that need a hydrated&lt;br&gt;
page have browser checks in &lt;code&gt;pages/checks/&lt;/code&gt;. On every pull request, headless&lt;br&gt;
Chromium presses the keys, moves focus, opens dialogs and toasts, and asserts&lt;br&gt;
what happens, instead of trusting the markup. The repository also has 139 test&lt;br&gt;
files (counted with &lt;code&gt;find . -name "*.test.ts*"&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendered on the server, then hydrated
&lt;/h2&gt;

&lt;p&gt;A component touches &lt;code&gt;window&lt;/code&gt; or &lt;code&gt;document&lt;/code&gt; only inside an effect or an event&lt;br&gt;
handler. So every one renders complete HTML on the server and hydrates in the&lt;br&gt;
browser. The live guide itself is served that way, and it reads with JavaScript&lt;br&gt;
off.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds. A page that shows its content before the&lt;br&gt;
JavaScript arrives is faster for the visitor and easier for a search engine. And&lt;br&gt;
a component that only works after hydration usually hides a bug that shows up on&lt;br&gt;
a slow phone.&lt;/p&gt;

&lt;p&gt;Colours, radii and fonts are CSS custom properties, so an app restyles&lt;br&gt;
everything by setting variables instead of forking the CSS. Layout goes through&lt;br&gt;
&lt;code&gt;Page&lt;/code&gt;, &lt;code&gt;Section&lt;/code&gt;, &lt;code&gt;Stack&lt;/code&gt;, &lt;code&gt;Cluster&lt;/code&gt; and &lt;code&gt;Grid&lt;/code&gt;, and no component carries an&lt;br&gt;
outer margin. The guide's header switches between light and dark and changes the&lt;br&gt;
accent colour live, so you can see the tokens at work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fkpi-dark-light.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fkpi-dark-light.webp" title="The same Kpi tiles in the guide's dark theme and its light theme." alt="The same five Kpi tiles from the live guide twice, each showing 42 in the accent, positive, warning, negative and neutral tones: on a dark background above and on a light one below" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is in the ten packages
&lt;/h2&gt;

&lt;p&gt;Every package is published on JSR under &lt;code&gt;@spy4x/preact-*&lt;/code&gt;, all at the same&lt;br&gt;
version, so a caret range always resolves to a set that was published together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it holds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the component set: layout, buttons, tables, dialogs, forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;design tokens, the Tailwind preset and the classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-icons&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the merged icon set, one component per glyph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-charts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;server-rendered charts with tooltips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;app-level pieces: auth form, calendar, shells, SEO head&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-crud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a list and editor scaffold for one collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a map on Leaflet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-signals&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stores and state helpers; no components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-cn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;class-name join with Tailwind conflict resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@spy4x/preact-ui-guide&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the live component catalogue, mounted in one line&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The guide's overview counts 81 components and 120 icons across them today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fline-chart-tooltip.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fantonshubin.com%2Fimg%2Fblog%2Fpreact-component-library-without-shadcn%2Fline-chart-tooltip.webp" title="A LineChart from @spy4x/preact-charts, with its tooltip open." alt="The LineChart card in the live guide: revenue today and a week earlier by hour, a dashed target line at 12, and a tooltip at 14:00 that shows no value for today and 10.9 for a week earlier" width="799" height="334"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it came from
&lt;/h2&gt;

&lt;p&gt;The components were extracted from real products, so I stop rebuilding the same&lt;br&gt;
pieces for the next one. My &lt;a href="https://dev.to/blog/deno-platform-template"&gt;SaaS template&lt;/a&gt;&lt;br&gt;
already builds its signed-in layout on &lt;code&gt;Shell&lt;/code&gt; from &lt;code&gt;@spy4x/preact-system&lt;/code&gt; and&lt;br&gt;
its styles on &lt;code&gt;@spy4x/preact-theme&lt;/code&gt;. This site is next: it still has its own&lt;br&gt;
components, and replacing them with the library is an open issue.&lt;/p&gt;

&lt;p&gt;The design is not mine. The design system, the component styling and the&lt;br&gt;
original markup are by &lt;a href="https://github.com/Eirene" rel="noopener noreferrer"&gt;Eirene&lt;/a&gt;&lt;br&gt;
(&lt;a href="https://isorokina.com/" rel="noopener noreferrer"&gt;isorokina.com&lt;/a&gt;). She designed it and built the&lt;br&gt;
original pages the components were first extracted from. What I added is the&lt;br&gt;
JavaScript: the Preact and signals implementation, the interactivity, server&lt;br&gt;
rendering, tests, packaging and documentation.&lt;/p&gt;

&lt;p&gt;Helpers with no UI, like dates and validation, come from my other library,&lt;br&gt;
&lt;a href="https://dev.to/tools/ts-libs"&gt;ts-libs&lt;/a&gt;, under the same &lt;code&gt;@spy4x&lt;/code&gt; scope on JSR.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it in a minute
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://spy4x.github.io/preact-components" rel="noopener noreferrer"&gt;live guide&lt;/a&gt;. Every&lt;br&gt;
component runs there with its code one click away, so you can try one before you&lt;br&gt;
install anything.&lt;/p&gt;

&lt;p&gt;When one looks right, add the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deno add jsr:@spy4x/preact-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And render it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Badge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@spy4x/preact-ui/badge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Active"&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"green"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One honest catch: the components render Tailwind classes against the compiled&lt;br&gt;
stylesheet from &lt;code&gt;@spy4x/preact-theme&lt;/code&gt;, and JSR cannot export a CSS file. So the&lt;br&gt;
stylesheet takes a short build script instead of a plain &lt;code&gt;@import&lt;/code&gt;. The theme&lt;br&gt;
package's README has the recipe, one short TypeScript file.&lt;/p&gt;

&lt;p&gt;The repository also has an &lt;code&gt;llms.txt&lt;/code&gt;, so a coding agent can read the whole&lt;br&gt;
component surface in one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not done yet
&lt;/h2&gt;

&lt;p&gt;I mark it beta on my site, and here is why. The version is 1.2.0 and it follows&lt;br&gt;
semantic versioning since 1.0.0, so a breaking change bumps the major number.&lt;br&gt;
But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No screen reader has been run against it yet.&lt;/strong&gt; The browser checks prove the
markup and the order in which the page changes, not what a screen reader
actually speaks. That work is an open issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only Deno is tested.&lt;/strong&gt; The packages are standard ES modules with no
Deno-only API, and a Vite app can use them through &lt;code&gt;@deno/vite-plugin&lt;/code&gt;.
Running them under Node or Bun has not been tried.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is Preact and Tailwind only.&lt;/strong&gt; If your app is React, or you don't use
Tailwind, skip it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some components are deliberately not built.&lt;/strong&gt; Carousel, Accordion, Drawer, a
rich-text editor and a tree view were considered and turned down: nothing I
run needs them, and some would need a large third-party piece I don't want.
The reasons are written down in the repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing in CI enforces the no-library rule.&lt;/strong&gt; A pull request could add Radix
and every check would pass. Review holds the rule, and the repository says so
instead of pretending a check exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What next?
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://spy4x.github.io/preact-components" rel="noopener noreferrer"&gt;live guide&lt;/a&gt;, press Tab&lt;br&gt;
through a dialog, and see if it behaves the way you expect. If something is&lt;br&gt;
missing or broken, open an issue on&lt;br&gt;
&lt;a href="https://github.com/spy4x/preact-components" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. And if the library looks&lt;br&gt;
useful, a star helps other Preact developers find it.&lt;/p&gt;

&lt;p&gt;Building a product and want its UI layer owned like this, instead of rented from&lt;br&gt;
a library? That's my day job: see&lt;br&gt;
&lt;a href="https://dev.to/catalog/zero-to-production-saas-mvp"&gt;how I build a SaaS MVP&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://antonshubin.com/blog/preact-component-library-without-shadcn?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=preact-component-library-without-shadcn" rel="noopener noreferrer"&gt;antonshubin.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Opus 5.5 vs Sonnet 5: the pricier model wrote my code for about half the cost</title>
      <dc:creator>Anton Shubin</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:54:16 +0000</pubDate>
      <link>https://dev.to/anton_shubin_f26b3cb5a572/opus-55-vs-sonnet-5-the-pricier-model-wrote-my-code-for-about-half-the-cost-2nn5</link>
      <guid>https://dev.to/anton_shubin_f26b3cb5a572/opus-55-vs-sonnet-5-the-pricier-model-wrote-my-code-for-about-half-the-cost-2nn5</guid>
      <description>&lt;p&gt;Opus 5.5 lists at twice the price of Sonnet 5. In my coding agents it cost about&lt;br&gt;
half as much per line of code. It also replaced Fable 5.1 as my code reviewer,&lt;br&gt;
at about a third of the price.&lt;/p&gt;

&lt;p&gt;This post is the data behind that switch: five days of my own Claude Code&lt;br&gt;
transcripts, priced at API list prices, with the parts that are proven kept&lt;br&gt;
apart from the parts that are only likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I run agents
&lt;/h2&gt;

&lt;p&gt;I work in waves. A lead model reads a list of GitHub issues and splits them into&lt;br&gt;
lanes that touch different files. Each lane gets an &lt;strong&gt;implementer&lt;/strong&gt; agent that&lt;br&gt;
writes the code in its own git worktree and opens a PR. A separate &lt;strong&gt;reviewer&lt;/strong&gt;&lt;br&gt;
agent then checks every PR: it runs the tests, breaks the code on purpose to see&lt;br&gt;
whether a test goes red, and checks the PR description's claims. Nothing merges&lt;br&gt;
until a reviewer passes it. A failed review ("needs-fix") goes back to the&lt;br&gt;
implementer for another round.&lt;/p&gt;

&lt;p&gt;Between September 20 and 25 that produced 203 implementer PRs and 411 reviewer&lt;br&gt;
agents across four models. I pay a flat subscription, so the dollars below are&lt;br&gt;
API list prices, used as a measure of how much of my usage limit each agent&lt;br&gt;
burned.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input $/M&lt;/th&gt;
&lt;th&gt;Output $/M&lt;/th&gt;
&lt;th&gt;Cached read $/M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fable 5.1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last column is the one that matters, and the next section shows why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money actually goes
&lt;/h2&gt;

&lt;p&gt;An agent doesn't pay for its work once. Every step is a new API call that sends&lt;br&gt;
the whole conversation again: the brief, every file it read, every test log.&lt;br&gt;
Most of that comes from the prompt cache, so each call is cheap, but an agent&lt;br&gt;
makes hundreds of them.&lt;/p&gt;

&lt;p&gt;In one six-hour window on September 24, my agents used about $593 at list&lt;br&gt;
prices. &lt;strong&gt;68% of that was cached reads&lt;/strong&gt;, 22% cache writes and 10% output.&lt;br&gt;
Across 57 agents, cost tracked "number of calls × average context size" almost&lt;br&gt;
perfectly (correlation 0.99). The worst offenders were Sonnet implementers that&lt;br&gt;
ran 400–600 calls while their context grew to 600–970K tokens, because nothing&lt;br&gt;
compacted it before about 967K. Every one of those calls re-read the whole&lt;br&gt;
thing.&lt;/p&gt;

&lt;p&gt;So the price per token matters less than you'd think. What matters is how many&lt;br&gt;
steps an agent takes and how much it carries through each one.&lt;/p&gt;

&lt;p&gt;Cached reads cost the same $0.20 per million tokens on Opus 5.5 and Sonnet 5.&lt;br&gt;
Only input, output and cache writes cost twice as much on Opus. For my Sonnet&lt;br&gt;
implementers, cached reads were about three quarters of the bill (the median&lt;br&gt;
lane), so the same call costs only about 25% more on Opus 5.5, not twice as&lt;br&gt;
much.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb56cgw3b71rx83ghwb63.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb56cgw3b71rx83ghwb63.png" alt="The same agent call priced on each model. Cached reads, three quarters of Sonnet's cost, are the same price on both, so Opus 5.5 costs 1.25 times as much per call, not 2 times" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementers: fewer steps beat a cheaper token
&lt;/h2&gt;

&lt;p&gt;Opus 5 only ran on September 20–21 and Opus 5.5 only on September 23–25, so each&lt;br&gt;
is compared with Sonnet 5 on the same days:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf3bzp9i1q7w88ylt35z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf3bzp9i1q7w88ylt35z.png" alt="Cost per 100 changed lines. September 20 to 21: Sonnet 5 $1.57, Opus 5 $2.45. September 23 to 25: Sonnet 5 $2.13, Opus 5.5 $0.39" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Days&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cost per 100 changed lines&lt;/th&gt;
&lt;th&gt;API calls per 100 lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sep 20–21&lt;/td&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;$1.57&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 20–21&lt;/td&gt;
&lt;td&gt;Opus 5&lt;/td&gt;
&lt;td&gt;$2.45&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 23–25&lt;/td&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;$2.13&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 23–25&lt;/td&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;$0.39&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Changed lines" is additions plus deletions in the PR. The median PR was the&lt;br&gt;
same size for Sonnet 5 and Opus 5.5 (484 and 504 lines), so Opus isn't winning&lt;br&gt;
by writing more lines. Over all five days, per 100 changed lines, Opus 5.5 made&lt;br&gt;
6 times fewer calls than Sonnet, read 10 times less context and wrote 3 times&lt;br&gt;
less output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opus 5 was not wasteful; its price was.&lt;/strong&gt; Its prices are exactly 2.5 times&lt;br&gt;
Sonnet's on every kind of token, so at Sonnet's prices it would have cost $0.98&lt;br&gt;
per 100 lines against Sonnet's $1.57. Opus 5.5 keeps that efficiency and drops&lt;br&gt;
the price.&lt;/p&gt;

&lt;h3&gt;
  
  
  The raw gap overstates it
&lt;/h3&gt;

&lt;p&gt;The table says Opus 5.5 cost a fifth of Sonnet. That isn't a fair comparison,&lt;br&gt;
because I chose which tasks went to Opus, and I gave it mostly authentication&lt;br&gt;
and crypto work. So I compared units of work of the same size and kind, in the&lt;br&gt;
same repository and the same session. Measured that way, &lt;strong&gt;Opus 5.5 cost about&lt;br&gt;
0.45 times what Sonnet did&lt;/strong&gt; (plausible range 0.29–0.71).&lt;/p&gt;

&lt;p&gt;That's still a big saving, and the more interesting part is where it comes from.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs52u0kaj6u3s2c3h94yc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs52u0kaj6u3s2c3h94yc.png" alt="Opus 5.5 cost relative to Sonnet 5, like for like. Up to the first review: 0.77 times, range 0.52 to 1.13, which includes equal cost. Whole PR including fix rounds: 0.45 times, range 0.29 to 0.71" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first draft costs about the same.&lt;/strong&gt; Up to the first review verdict, Opus&lt;br&gt;
5.5 cost about 0.77 times Sonnet, and that range includes equal cost. Opus did&lt;br&gt;
bundle its work into fewer, longer shell commands: a median of 31 calls before&lt;br&gt;
the first review, against Sonnet's 97.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The saving is in the fix rounds.&lt;/strong&gt; In the September 24 window, fix rounds were&lt;br&gt;
about 71% of implementer spend. A Sonnet implementer that had already grown a&lt;br&gt;
900K context paid $5.60–6.40 for a small last-round fix, because it re-read&lt;br&gt;
everything to make it. Opus carried a smaller context (a median peak of 170K&lt;br&gt;
against Sonnet's 246K), and in the 5 Opus lanes of the fair split I describe at&lt;br&gt;
the end it needed a median of 2 review rounds against Sonnet's 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most PRs fail their first review
&lt;/h3&gt;

&lt;p&gt;On ordinary code, most PRs from every model failed their first review. Sonnet 5&lt;br&gt;
and Opus 5 passed 12–20% of the time. In one six-hour window on September 24,&lt;br&gt;
all 20 PRs that changed production code failed their first review.&lt;/p&gt;

&lt;p&gt;Opus 5.5's raw first-review pass rate looks great: 10 of 18. But almost all of&lt;br&gt;
those passes were auth work that I had routed to Opus and that Fable reviewed.&lt;br&gt;
On other code, Opus 5.5 passed 1 of 7, which is too few to say anything. Look at&lt;br&gt;
why PRs fail, and the model matters less: at least 7 of those 20 failures were&lt;br&gt;
tests that stayed green on broken code, or a PR description that claimed&lt;br&gt;
something untrue. I don't expect a smarter model to fix that, so I changed the&lt;br&gt;
process instead; it's too early to know whether that worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  More reasoning effort cost more and bought nothing
&lt;/h2&gt;

&lt;p&gt;Until this week, my implementers ran at whatever effort their lead used, so the&lt;br&gt;
past lanes cover several levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementer&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Cost per 100 changed lines&lt;/th&gt;
&lt;th&gt;Passed first review&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;$1.72&lt;/td&gt;
&lt;td&gt;8 of 64 (12%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;$2.52&lt;/td&gt;
&lt;td&gt;9 of 53 (17%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;xhigh&lt;/td&gt;
&lt;td&gt;$2.56&lt;/td&gt;
&lt;td&gt;1 of 13 (8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;$0.39&lt;/td&gt;
&lt;td&gt;9 of 16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sonnet at high effort cost 35–51% more per line than at medium, with no clear&lt;br&gt;
gain in review results. Opus 5.5 did its auth and crypto work at medium. This is&lt;br&gt;
observational (repositories ran at different efforts), so it's a signal, not&lt;br&gt;
proof. It was enough to stop me guessing effort up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reviewers: Opus 5.5 against Fable 5.1
&lt;/h2&gt;

&lt;p&gt;On the same kind of PR in the same days, with both priced at Opus 5.5's rates, a&lt;br&gt;
review round cost $1.36 on Opus 5.5 and $1.50 on Fable 5.1: about the same&lt;br&gt;
amount of work. At real list prices, Fable cost about 2.7 times more, and it was&lt;br&gt;
slower (a median of 9 minutes a round against 6.5).&lt;/p&gt;

&lt;p&gt;Fable wasn't stricter either. On Sonnet-written code in the same days, it&lt;br&gt;
blocked 17 of 19 first submissions, and Opus 5.5 blocked 74 of 92. That is no&lt;br&gt;
real difference.&lt;/p&gt;

&lt;p&gt;On the four PRs that both reviewed at the same time, &lt;strong&gt;each caught security bugs&lt;br&gt;
the other missed&lt;/strong&gt;. One was a delete path that followed symbolic links: Fable&lt;br&gt;
caught it, and the Opus review running in parallel did not. So neither reviewer&lt;br&gt;
is complete on its own. Instead of paying 2.7 times more for a second opinion, I&lt;br&gt;
added that class of bug to the reviewer's checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus 5.5 implements and reviews every diff.&lt;/strong&gt; Fable stays paused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium effort by default.&lt;/strong&gt; Reviews of production code run at xhigh, and
reviews of auth, crypto, secrets, money, deploys and data migrations run at
max. An implementer only moves up to xhigh for one fix round, when a PR's
second failed review names a real behaviour defect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compact at 400K tokens instead of about 967K.&lt;/strong&gt; The single largest saving in
the analysis: it caps how much every call has to re-read. Reviewers, which
peaked at 277K in the window I measured, never hit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 1-hour cache for reviewers.&lt;/strong&gt; Re-reviews usually arrive after the default
5-minute cache has expired, so the reviewer paid to rebuild its whole context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof for every test.&lt;/strong&gt; Each test a PR adds or changes needs a line in the
PR body naming the code change that turns it red. A PR without those lines
goes back before any reviewer spends a token on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this doesn't prove
&lt;/h2&gt;

&lt;p&gt;It isn't a controlled experiment. I chose which tasks went to Opus, the sample&lt;br&gt;
is small (19 Opus 5.5 PRs), and list prices are a stand-in for subscription&lt;br&gt;
usage. I started a fair split, with every other lane on Opus 5.5 regardless of&lt;br&gt;
the task, and stopped it early: the like-for-like comparison and the token&lt;br&gt;
counts pointed the same way. Opus 5.5 was cheaper, probably around half, and at&lt;br&gt;
worst no more expensive.&lt;/p&gt;

&lt;p&gt;One gap favours Opus in these numbers. In at least 3 of the 8 Opus PRs that&lt;br&gt;
failed their first review, the lead agent made the fix itself, so that fix's&lt;br&gt;
cost is missing from Opus's total. That happened in only 1 of 89 Sonnet cases.&lt;/p&gt;

&lt;p&gt;The lesson I'd keep even if the model prices change next month: in agent work,&lt;br&gt;
you pay for steps and context, not for tokens. Measure calls and context per&lt;br&gt;
unit of work, and the right model is usually obvious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
