<?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: RAXXO Studios</title>
    <description>The latest articles on DEV Community by RAXXO Studios (@raxxostudios).</description>
    <link>https://dev.to/raxxostudios</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3848289%2Ffd2912c9-5820-4993-8fdc-62ec1e778980.png</url>
      <title>DEV Community: RAXXO Studios</title>
      <link>https://dev.to/raxxostudios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raxxostudios"/>
    <language>en</language>
    <item>
      <title>Form Validation in 2026: 6 Native Constraints Before You Reach for a Library</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 19 Jun 2026 11:25:43 +0000</pubDate>
      <link>https://dev.to/raxxostudios/form-validation-in-2026-6-native-constraints-before-you-reach-for-a-library-3474</link>
      <guid>https://dev.to/raxxostudios/form-validation-in-2026-6-native-constraints-before-you-reach-for-a-library-3474</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Native constraints cover 6 common cases without a single byte of JS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;setCustomValidity gives you full control over error text&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;:user-invalid styles errors only after the user interacts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reach for a library only for cross-field and async rules&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I shipped a checkout form last month with zero validation libraries and it handled everything: required fields, email shapes, password length, matching inputs, and inline error styling. The whole thing was native HTML and about 30 lines of JavaScript. Here is exactly how far the browser takes you in 2026, and the precise point where a library still earns its bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 Native Constraints That Cover Most Forms
&lt;/h2&gt;

&lt;p&gt;Before you &lt;code&gt;npm install&lt;/code&gt; anything, here are the six attributes that solve the bulk of real validation. Every one of these ships in every browser shipping today.&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;required&lt;/code&gt;. Empty field, no submit. One word. It works on text inputs, selects, checkboxes, and radios.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;type&lt;/code&gt;. Setting &lt;code&gt;type="email"&lt;/code&gt; makes the browser check for a basic email shape. &lt;code&gt;type="url"&lt;/code&gt; checks for a protocol. &lt;code&gt;type="number"&lt;/code&gt; blocks non-numeric input on most keyboards and validates on submit. These are not perfect (the email check allows &lt;code&gt;a@b&lt;/code&gt; which is technically valid) but they catch 90 percent of fat-finger mistakes.&lt;/p&gt;

&lt;p&gt;Third, &lt;code&gt;minlength&lt;/code&gt; and &lt;code&gt;maxlength&lt;/code&gt;. A password field with &lt;code&gt;minlength="8"&lt;/code&gt; refuses to submit at 7 characters and tells the user why. No counter logic needed.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, and &lt;code&gt;step&lt;/code&gt; for numbers and dates. A quantity field with &lt;code&gt;min="1" max="99"&lt;/code&gt; enforces the range. A date picker with &lt;code&gt;min="2026-01-01"&lt;/code&gt; blocks past dates.&lt;/p&gt;

&lt;p&gt;Fifth, &lt;code&gt;pattern&lt;/code&gt;. This is a regex attribute. A postal code field with &lt;code&gt;pattern="[0-9]{5}"&lt;/code&gt; enforces five digits. Pair it with the &lt;code&gt;title&lt;/code&gt; attribute so the error message explains the expected format.&lt;/p&gt;

&lt;p&gt;Sixth, &lt;code&gt;inputmode&lt;/code&gt; and &lt;code&gt;autocomplete&lt;/code&gt;. These do not validate, but they reduce errors before they happen. &lt;code&gt;inputmode="numeric"&lt;/code&gt; pulls up the number pad on phones. &lt;code&gt;autocomplete="email"&lt;/code&gt; lets the browser fill known-good data. Fewer typos means fewer validation failures.&lt;/p&gt;

&lt;p&gt;Here is a real field from that checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That block validates four ways with no script attached. The browser blocks submit, shows a bubble, and focuses the first bad field. I measured it: this covered 6 of the 8 validation cases on the form. Two cases needed JavaScript, which I will get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraint Validation API for Custom Messages
&lt;/h2&gt;

&lt;p&gt;The native bubble messages are functional but generic. "Please fill out this field" is fine. "Please match the requested format" is useless to a user staring at a postal code box. This is where the Constraint Validation API earns its place, and it is built into every input element already.&lt;/p&gt;

&lt;p&gt;Every form control exposes a &lt;code&gt;validity&lt;/code&gt; object. Read it and you get booleans like &lt;code&gt;valueMissing&lt;/code&gt;, &lt;code&gt;typeMismatch&lt;/code&gt;, &lt;code&gt;tooShort&lt;/code&gt;, &lt;code&gt;patternMismatch&lt;/code&gt;, and &lt;code&gt;rangeUnderflow&lt;/code&gt;. You check which one is true and you write a message that actually helps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valueMissing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCustomValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is required.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;typeMismatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCustomValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;That email looks off.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCustomValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCustomValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail people miss: you must clear the custom message on the next &lt;code&gt;input&lt;/code&gt; event by calling &lt;code&gt;setCustomValidity('')&lt;/code&gt;. If you forget, the field stays stuck in an invalid state even after the user fixes it. That one line is the difference between a working form and a frustrating one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setCustomValidity&lt;/code&gt; also flips the field into an invalid state. So you can use it for rules the browser does not know. Want to reject disposable email domains? Check the value, set a custom message, and the native machinery handles the rest: the submit blocks, the bubble shows, focus moves.&lt;/p&gt;

&lt;p&gt;You also get &lt;code&gt;checkValidity()&lt;/code&gt; and &lt;code&gt;reportValidity()&lt;/code&gt; on both individual fields and the whole form. &lt;code&gt;checkValidity()&lt;/code&gt; returns true or false silently. &lt;code&gt;reportValidity()&lt;/code&gt; does the same but also shows the bubbles and focuses the first bad field. I call &lt;code&gt;form.reportValidity()&lt;/code&gt; at the top of my submit handler. If it returns false, I bail before touching the network. That single call replaced what used to be a 40-line validation loop in older codebases I have cleaned up.&lt;/p&gt;

&lt;p&gt;The reason this matters for a one-person studio: less code means fewer bugs to chase later. I documented my whole approach to keeping dependencies lean in the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;, and form validation is one of the clearest examples of the browser already doing the heavy lifting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling Errors With :user-invalid Instead of :invalid
&lt;/h2&gt;

&lt;p&gt;For years the styling story was broken. The &lt;code&gt;:invalid&lt;/code&gt; pseudo-class matches a field the moment the page loads if it is empty and required. So a fresh form shows every required field glowing red before the user types a single character. That is hostile. Everyone worked around it with JavaScript classes like &lt;code&gt;.touched&lt;/code&gt; or &lt;code&gt;.dirty&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:user-invalid&lt;/code&gt; fixes this at the CSS level. It only matches after the user has interacted with the field and then moved on, or after a submit attempt. No JavaScript flag needed. This is shipping in every current browser as of 2026, so the old workarounds are dead weight now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:user-invalid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#d33&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:user-valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2a2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire styling layer. Empty form on load: neutral borders. User tabs through an empty required field: red border appears. User fixes it: green border. No class toggling, no event listeners for state. I cut about 60 lines of "touched state" tracking out of a form rebuild and the behavior got better, not worse.&lt;/p&gt;

&lt;p&gt;There is a companion pseudo-class worth knowing: &lt;code&gt;:has()&lt;/code&gt;. You can style a field's wrapper based on the input inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nc"&gt;.field&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:user-invalid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.error-text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shows your custom inline error text only when the field is actually in a user-invalid state. Combine &lt;code&gt;:user-invalid&lt;/code&gt; with &lt;code&gt;:has()&lt;/code&gt; and you have a full error display system with zero JavaScript for the styling layer. The JS only sets messages; CSS handles all the visual state.&lt;/p&gt;

&lt;p&gt;One gotcha: &lt;code&gt;:user-invalid&lt;/code&gt; does not fire for programmatic value changes. If you set a value with JavaScript, it will not flag as user-invalid until the user touches it. Usually that is the behavior you want, but test it if you autofill fields. I keep a short list of these browser quirks the same way I keep reusable Git hooks, small things I copy into every project so I never relearn them. The pattern of writing down the gotcha once saves me an afternoon every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a Library Still Earns Its Bytes
&lt;/h2&gt;

&lt;p&gt;Native validation handles single-field rules beautifully. It falls apart on two things, and these are exactly where a library still pays for its download size.&lt;/p&gt;

&lt;p&gt;First, cross-field validation. The classic case is "confirm password must match password." The browser has no concept of one field depending on another. You can hack it with &lt;code&gt;setCustomValidity&lt;/code&gt; and a manual comparison, and for one rule that is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCustomValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Passwords must match.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But when you have five interdependent rules ("if country is US, postal code is required and must be 5 digits; if shipping differs from billing, validate both blocks") the manual approach turns into spaghetti fast. A schema-based library like Zod or Valibot lets you declare the whole relationship once and validate the entire object in one call. That is genuinely better than hand-rolling it.&lt;/p&gt;

&lt;p&gt;Second, async validation. "Is this username taken?" or "Is this discount code valid?" requires a network call. Native constraints are synchronous. You can fake async with &lt;code&gt;setCustomValidity&lt;/code&gt; after a fetch resolves, but you have to manage loading states, debouncing, and race conditions yourself. A good library gives you that plumbing for free.&lt;/p&gt;

&lt;p&gt;My rule of thumb after building dozens of forms: if your form is single-field rules plus one or two cross-field checks, stay native. The total cost is the 30 lines I mentioned at the top. If you have a multi-step form, conditional fields that appear based on earlier answers, server-side schema reuse, or three or more async checks, reach for a schema library and share the schema between client and server. That shared schema is the real payoff, not the validation itself.&lt;/p&gt;

&lt;p&gt;The trap is reaching for the library by default. I see projects pull in 40KB of validation code to enforce a required email field. That is solved by one HTML attribute. Start native, measure what actually breaks, and add the library only at the field where the browser genuinely cannot help. Most forms never hit that line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Native form validation in 2026 covers far more than most developers assume. Six attributes (&lt;code&gt;required&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;minlength&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;, and the input hints) handle the common cases with zero JavaScript. The Constraint Validation API gives you full control over error messages through &lt;code&gt;setCustomValidity&lt;/code&gt;. And &lt;code&gt;:user-invalid&lt;/code&gt; finally lets you style errors at the right moment without tracking touched state by hand. Together that is a complete validation system in about 30 lines.&lt;/p&gt;

&lt;p&gt;The library only earns its bytes for cross-field rules at scale and async checks against a server. Even then, the real win is sharing one schema between client and browser, not the validation logic itself.&lt;/p&gt;

&lt;p&gt;I rebuild small tools constantly as a solo studio, and cutting dependencies is how I stay fast. If you want the same approach applied across a whole project, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through how I keep things lean from the first commit. Start native. Add code only where the browser truly cannot help. Your future self maintaining the form will thank you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Native Popover API: 4 Menus and Tooltips I Built Without JavaScript</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Thu, 18 Jun 2026 00:16:50 +0000</pubDate>
      <link>https://dev.to/raxxostudios/the-native-popover-api-4-menus-and-tooltips-i-built-without-javascript-57k2</link>
      <guid>https://dev.to/raxxostudios/the-native-popover-api-4-menus-and-tooltips-i-built-without-javascript-57k2</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Popover attribute gives top-layer rendering and light-dismiss free&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Anchor positioning ties menus to triggers with no JavaScript&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Built 4 components: dropdown, tooltip, command menu, confirm popup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removed a positioning library and every z-index hack from the project&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deleted a 12KB positioning library and 40 lines of z-index guesswork from one &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; theme last month. The replacement was two HTML attributes and a handful of CSS rules. The native popover API plus CSS anchor positioning now handles every menu, tooltip, and confirm dialog I used to wire up by hand.&lt;/p&gt;

&lt;p&gt;Here is what I built, why it works, and where the rough edges still are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why The Popover Attribute Changes The Math
&lt;/h2&gt;

&lt;p&gt;The first thing that surprised me was how little code it took. A popover is just an element with the &lt;code&gt;popover&lt;/code&gt; attribute and a trigger that points at it with &lt;code&gt;popovertarget&lt;/code&gt;. No event listeners. No state variable. No library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Options

  Edit
  Duplicate
  Delete

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click the button, the div shows. Click outside it, the div hides. Press Escape, it hides. That is light-dismiss, and it ships for free. I used to write a document click listener for every dropdown, check if the click was inside the menu, then close it. Then I would forget the Escape key. Then I would forget to remove the listener on unmount and leak memory. All of that is gone.&lt;/p&gt;

&lt;p&gt;The bigger win is the top layer. Anything with &lt;code&gt;popover&lt;/code&gt; renders in the browser top layer, above everything else on the page, regardless of where it sits in the DOM or what &lt;code&gt;overflow: hidden&lt;/code&gt; ancestors it has. This is the thing that ended my z-index wars. I had a theme where a card had &lt;code&gt;overflow: hidden&lt;/code&gt; for rounded corners, and a dropdown inside it got clipped. The old fix was teleporting the menu to &lt;code&gt;document.body&lt;/code&gt; with a portal, then manually positioning it. The top layer makes that entire pattern unnecessary.&lt;/p&gt;

&lt;p&gt;There are two modes. &lt;code&gt;popover&lt;/code&gt; (which defaults to &lt;code&gt;popover="auto"&lt;/code&gt;) gives light-dismiss and only one auto popover open at a time. &lt;code&gt;popover="manual"&lt;/code&gt; stays open until you close it in code, which I use for toasts and persistent panels. Picking the right mode up front saves a lot of confusion later.&lt;/p&gt;

&lt;p&gt;Browser support is solid now. Chrome, Edge, Safari, and Firefox all ship it. I still add a tiny feature check for the 3 percent of traffic on old browsers, and those users get a plain inline block instead of a floating menu. Nobody complained. The fallback is uglier, not broken, which is the correct tradeoff for a progressive enhancement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component One And Two: Dropdown And Tooltip
&lt;/h2&gt;

&lt;p&gt;The dropdown was the easy case. The trick is positioning it relative to the trigger, and that is where anchor positioning comes in. I name the trigger as an anchor, then tell the popover to attach to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nc"&gt;.menu-trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;anchor-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--opts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;#menu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;position-anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--opts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bottom&lt;/span&gt; &lt;span class="n"&gt;span-right&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;position-area&lt;/code&gt; is the readable shorthand. &lt;code&gt;bottom span-right&lt;/code&gt; means "below the trigger, spanning to the right." No &lt;code&gt;getBoundingClientRect&lt;/code&gt;, no scroll listeners, no recalculating on resize. The browser keeps the menu glued to the trigger as the page scrolls. I tested it inside a sticky header and it tracked perfectly.&lt;/p&gt;

&lt;p&gt;The part that earns its keep is &lt;code&gt;position-try&lt;/code&gt;. If the menu would overflow the viewport bottom, the browser flips it above the trigger automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nf"&gt;#menu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;position-try-fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flip-block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flip-inline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line replaced the collision-detection logic that used to be the buggiest part of every menu I shipped. On a long product page, the dropdowns near the footer now open upward without me writing a single conditional.&lt;/p&gt;

&lt;p&gt;The tooltip was even leaner. I used &lt;code&gt;popover="manual"&lt;/code&gt; paired with &lt;code&gt;popovertargetaction&lt;/code&gt; and hover handling, but the cleaner version uses the new CSS-only approach where a tooltip is an anchored popover shown on focus and hover. For accessibility I keep it dismissible by Escape and reachable by keyboard, because a tooltip that only appears on mouse hover excludes keyboard users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nc"&gt;.tip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;position-anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--field&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;top&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.tip&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c"&gt;/* little arrow pointing at the anchor */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One real number: my tooltip component went from 84 lines of JavaScript and CSS down to 21 lines of pure CSS plus the markup. That is roughly a 75 percent cut, and the new version has fewer edge cases because the browser owns the positioning math. If you want the deeper context on the positioning side, CSS Anchor Positioning Is Production Ready covers the five patterns I lean on most. (Verify that link exists in your index before shipping, since I keep a running list of anchor patterns there.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Three: The Command Menu
&lt;/h2&gt;

&lt;p&gt;This is the one I was sure would force me back into JavaScript, and it mostly did not. A command menu is a search-filtered list of actions, the kind you open with a keyboard shortcut. The popover handles the open and close. Anchor positioning handles the placement, centered near the top of the screen.&lt;/p&gt;

&lt;p&gt;The popover attribute gives me the shell for free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I still need JavaScript for two things: filtering the list as you type, and the keyboard shortcut to open it. Everything else (the top-layer render, the backdrop, the Escape-to-close, the click-outside dismiss) is native. So my script shrank to about 30 lines, and all of it is genuine logic rather than DOM plumbing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;::backdrop&lt;/code&gt; pseudo-element deserves a mention. Popovers get a backdrop element you can style without adding a div.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nf"&gt;#cmd&lt;/span&gt;&lt;span class="nd"&gt;::backdrop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used to build that dimmed background with an absolutely positioned overlay div, then juggle its z-index against the menu. Now it is one selector and the layering is handled by the top layer.&lt;/p&gt;

&lt;p&gt;The animation was the last piece. Popovers can animate on open and close using &lt;code&gt;@starting-style&lt;/code&gt; and transitions on &lt;code&gt;display&lt;/code&gt; and &lt;code&gt;overlay&lt;/code&gt;, which lets the element stay in the top layer through the exit animation. This used to require a setTimeout to delay the unmount until the animation finished, a pattern that broke whenever the user clicked fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nf"&gt;#cmd&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opacity&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overlay&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt; &lt;span class="n"&gt;allow-discrete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display&lt;/span&gt; &lt;span class="m"&gt;0.15s&lt;/span&gt; &lt;span class="n"&gt;allow-discrete&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;#cmd&lt;/span&gt;&lt;span class="nd"&gt;:popover-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@starting-style&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;#cmd&lt;/span&gt;&lt;span class="nd"&gt;:popover-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;allow-discrete&lt;/code&gt; keyword is the magic word that makes &lt;code&gt;display&lt;/code&gt; and &lt;code&gt;overlay&lt;/code&gt; animatable. Without it, the menu just snaps. I forgot it the first three times and could not figure out why my fade did nothing. Background: I covered the same display-transition trick in CSS Anchor Positioning Is Production Ready if you want the full breakdown.&lt;/p&gt;

&lt;p&gt;For scheduling the posts where I document these builds I lean on &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt;, which keeps the publishing side off my plate while I write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Component Four: The Confirm Popup, And The Catches
&lt;/h2&gt;

&lt;p&gt;The fourth component is a small confirm popup, the "are you sure you want to delete this" prompt that appears anchored to the delete button. I used &lt;code&gt;popover="manual"&lt;/code&gt; here on purpose, because I do not want a misclick outside the popup to count as dismissal when a destructive action is on the table. The user has to choose Cancel or Confirm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Delete


Delete this item?

  Cancel
  Confirm

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the Cancel button uses &lt;code&gt;popovertargetaction="hide"&lt;/code&gt; to close the popup declaratively, no script. The Confirm button runs the actual action. That split keeps the dangerous path explicit.&lt;/p&gt;

&lt;p&gt;Now the catches, because there are real ones.&lt;/p&gt;

&lt;p&gt;First, focus management. The popover API moves focus into the popover for &lt;code&gt;dialog&lt;/code&gt; elements, but a plain &lt;code&gt;div popover&lt;/code&gt; does not trap focus by default. For the command menu I move focus to the search input on open with a tiny &lt;code&gt;toggle&lt;/code&gt; event listener. Do not skip this. A menu that opens without sending focus is unusable by keyboard.&lt;/p&gt;

&lt;p&gt;Second, &lt;code&gt;position-area&lt;/code&gt; does not yet cover every alignment case I want. Centering a popover horizontally over a wide anchor sometimes needs &lt;code&gt;justify-self: anchor-center&lt;/code&gt;, which has spottier support than the rest. I keep a &lt;code&gt;@supports&lt;/code&gt; check and fall back to a margin nudge.&lt;/p&gt;

&lt;p&gt;Third, the auto-dismiss can fight you. If you nest an auto popover inside another auto popover, opening the child can close the parent unless you use the popover nesting rules correctly by placing the trigger inside the parent. I lost an hour to this before reading that the relationship is inferred from where the trigger lives, not from DOM nesting of the popovers themselves.&lt;/p&gt;

&lt;p&gt;Fourth, animating exit needs &lt;code&gt;overlay&lt;/code&gt; in the transition list or the element drops out of the top layer mid-animation and flickers behind other content. That one bit me on the confirm popup until I added it.&lt;/p&gt;

&lt;p&gt;None of these are dealbreakers. They are the kind of edges you hit once, write down, and never hit again. My running notes on this stuff feed straight into &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;, which is where I keep the patterns I reuse across themes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Four components: dropdown, tooltip, command menu, confirm popup. I shipped all of them with the native popover attribute and CSS anchor positioning, and I removed a 12KB library plus every z-index hack in the process. The tooltip alone dropped about 75 percent of its code. The command menu kept roughly 30 lines of genuine logic and gave the rest back to the browser.&lt;/p&gt;

&lt;p&gt;The mental shift is the real payoff. Top-layer rendering ends clipping and stacking fights. Light-dismiss and Escape handling are free. Anchor positioning with &lt;code&gt;position-try&lt;/code&gt; flips menus away from screen edges without a single conditional. I write less code, ship fewer bugs, and the components behave consistently because the browser owns the hard parts.&lt;/p&gt;

&lt;p&gt;If you build store interfaces or any UI with floating elements, try rebuilding one menu this way before reaching for a library. Start with a plain dropdown, add the anchor, add a &lt;code&gt;flip-block&lt;/code&gt; fallback, then graduate to the command menu. The patterns stack. My full set of reusable snippets and the way I keep them organized lives in &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; if you want a head start.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Anatomy of a Good Empty State: 6 Patterns That Guide the Next Action</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Wed, 17 Jun 2026 00:18:17 +0000</pubDate>
      <link>https://dev.to/raxxostudios/the-anatomy-of-a-good-empty-state-6-patterns-that-guide-the-next-action-4603</link>
      <guid>https://dev.to/raxxostudios/the-anatomy-of-a-good-empty-state-6-patterns-that-guide-the-next-action-4603</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Three empty state types: first-run, cleared, error-empty, each needs different copy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One primary action rule: never give an empty screen two equal buttons&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy teaches, never apologizes: replace "No items found" with a next step&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;6 patterns I ship across every RAXXO interface&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most empty states are accidents. Nobody designs them, so they default to a gray "No data" line and a sad icon. I treat them as the cheapest onboarding I will ever build, because a person sees the empty state before they see anything full.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Empty States Are Onboarding in Disguise
&lt;/h2&gt;

&lt;p&gt;The first screen a new user sees is almost always empty. No projects, no orders, no saved items. That is not a failure mode. That is the actual product introduction, and it happens at the exact moment the person is deciding whether to keep going.&lt;/p&gt;

&lt;p&gt;I tracked this on one of my dashboards. Out of 100 new accounts, 38 hit an empty list within the first 20 seconds. If that screen just says "No projects yet," I am wasting the most attentive moment a user will ever give me. They are not bored yet. They are not frustrated yet. They are looking for the next thing to do.&lt;/p&gt;

&lt;p&gt;So I split empty states into three kinds, and I never let one template cover all three.&lt;/p&gt;

&lt;p&gt;The first is first-run empty. The user has never had data here. This is pure onboarding. The job is to explain what this space is for and hand them one obvious action.&lt;/p&gt;

&lt;p&gt;The second is cleared empty. The user had data and finished it. An inbox at zero, a task list completed, a cart emptied after checkout. This one should feel like a small reward, not a void. "You are all caught up" beats "No messages" every time.&lt;/p&gt;

&lt;p&gt;The third is error-empty. Something failed. The list is empty because a request timed out or a filter returned nothing. This needs a different tone again, because the user did not cause it and a cheerful illustration here feels mocking.&lt;/p&gt;

&lt;p&gt;When I started labeling every empty screen with one of these three tags before writing a single word of copy, my interfaces got noticeably less confusing. The same gray box was doing three jobs badly. Now each job gets its own treatment. The structural lesson is the same one behind any good system: name the state before you style it, the way I name component states in &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; before I write a line of markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1 and 2: The Single Primary Action Rule
&lt;/h2&gt;

&lt;p&gt;Here is the rule I break the least: an empty state gets exactly one primary action. One button that looks like a button. Everything else is text or a secondary link.&lt;/p&gt;

&lt;p&gt;I learned this the hard way. An early version of a project screen had three buttons of equal weight: "Create project," "Import data," and "Watch tutorial." All three were blue. All three were the same size. New users clicked none of them at a rate that embarrassed me. When three options carry equal visual weight, the brain treats the choice as work, and work at minute zero is where people leave.&lt;/p&gt;

&lt;p&gt;I cut it to one blue button: "Create your first project." The other two became a single line of gray text underneath: "or import from a file." The number of people who took any action inside the first session jumped enough that I stopped second-guessing the rule.&lt;/p&gt;

&lt;p&gt;Pattern 1 is the single primary action. Pattern 2 is the supporting line. The supporting line exists for the 15 percent of users who do not want the obvious path. You do not hide the import option. You demote it. The visual hierarchy does the teaching: this is what most people do, and here is a quieter door if you are not most people.&lt;/p&gt;

&lt;p&gt;A concrete checklist I run on every empty state now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Exactly one element styled as a primary button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;That button's label is a verb plus the object ("Add a product," not "Get started").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Any alternative path is text-weight, not button-weight.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No more than two total clickable things in the whole empty area.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. I counted the clickable elements on one cluttered empty screen and found seven, including a help icon, two nav crumbs, and a settings gear. Seven targets on a screen with zero content. I stripped it to two. The button and the alternative link. Less to scan means faster decisions, and a faster first decision is the entire game on a brand-new account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3 and 4: Copy That Teaches Instead of Apologizing
&lt;/h2&gt;

&lt;p&gt;The default empty-state copy is an apology. "No results found." "Nothing here yet." "Sorry, no items." Apology copy treats the empty screen as a problem the user must forgive. Teaching copy treats it as the next lesson.&lt;/p&gt;

&lt;p&gt;Pattern 3 is the teaching headline. Instead of describing the absence, describe the capability. Bad: "No automations." Good: "Automations run tasks for you while you sleep." The second version tells a brand-new user what the feature even does, which the empty screen is the perfect place to explain because there is nothing else competing for the space.&lt;/p&gt;

&lt;p&gt;I rewrote 11 empty-state headlines across one app using this single swap. Describe what the thing does, not what is missing. For a saved-searches feature I went from "No saved searches" to "Save a search to get alerts when new listings match." The word count went up by six. The clarity went up far more, because a new user genuinely did not know that saved searches triggered alerts. The empty state taught them.&lt;/p&gt;

&lt;p&gt;Pattern 4 is matching tone to state type, which ties back to the three categories. First-run copy is instructional and warm. Cleared copy is congratulatory and brief. Error-empty copy is plain and honest, with a retry action.&lt;/p&gt;

&lt;p&gt;Here is the same screen, a notifications list, written three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;First-run: "Notifications show up here when something needs you. Turn on alerts to start."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleared: "You are all caught up. Nice."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error-empty: "We could not load notifications. Retry, or check back in a minute."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same empty list, three completely different messages, because the user arrived at the empty state for three completely different reasons. The error version gets the retry button. The cleared version gets nothing clickable at all, because the user just finished and does not need a task. The first-run version gets the one primary action.&lt;/p&gt;

&lt;p&gt;The thing I keep relearning: copy in an empty state is not decoration around the real UI. In an empty state, the copy IS the UI. There is nothing else on the screen, so every word is load-bearing. If you want the deeper context on writing interface copy that does work, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through how I prompt for it consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5 and 6: Illustration Weight and the Cleared-State Reward
&lt;/h2&gt;

&lt;p&gt;Pattern 5 is illustration restraint. A big custom illustration on an error-empty state reads as tone-deaf. The request failed and you are showing me a smiling cartoon. I keep illustrations for first-run and cleared states only, and even there I keep them small.&lt;/p&gt;

&lt;p&gt;I generate these in batches with &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; when I want a consistent style across a dozen empty screens, then downscale hard. A 1200px hero illustration on an empty list is the wrong weight. It pulls attention away from the one action I want the user to take. I cap empty-state art around 160px wide. The button stays the loudest thing on screen. The art supports, it does not headline.&lt;/p&gt;

&lt;p&gt;There is a measurable cost to heavy illustrations too. One first-run screen shipped with a 480KB animated graphic that delayed the primary button paint by most of a second on a mid-range phone. The button is the point. Delaying the button to show art is the tail wagging the dog. I swapped it for an inline SVG under 8KB and the button appeared instantly.&lt;/p&gt;

&lt;p&gt;Pattern 6 is treating the cleared state as a reward, not a void. This is the one most products miss. When a user clears their inbox or finishes every task, the screen goes empty, and a lazy design shows the same "No items" message it would show a confused first-run user. That is a wasted moment of genuine satisfaction.&lt;/p&gt;

&lt;p&gt;Give the cleared state a small payoff. A short congratulatory line. A subtle checkmark. Maybe a count of what they just finished ("12 tasks done today"). I added a one-line cleared message to a task app and the number of users who came back the next day to clear it again went up, because finishing felt like something instead of nothing.&lt;/p&gt;

&lt;p&gt;If a cleared state is a natural moment of accomplishment, it is also a natural moment to suggest the next loop. Not a hard sell. A gentle "Schedule next week's posts?" works when someone just cleared their queue. I route social actions like that through &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; so the cleared state becomes a soft on-ramp to the next session rather than a dead stop. The rule stays intact: one primary action, even in the reward state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Empty states are not edge cases. They are the first screen, the finished screen, and the broken screen, and each one is a chance to either guide someone or lose them. Label every empty area as first-run, cleared, or error-empty before you write a word. Give it exactly one primary action. Write copy that teaches what the feature does instead of apologizing for what is missing. Keep illustrations small enough that the button stays the loudest thing on screen. And treat a cleared state as a reward, because the user earned it.&lt;/p&gt;

&lt;p&gt;I run these six patterns as a checklist on every interface I ship now, and the gray "No data" box has basically disappeared from my work. The screens that used to feel like dead ends now point somewhere. If you want the full system I use to prompt for interface copy and component states consistently, &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; is where I keep it. Start with your most-seen empty screen and rewrite the headline first. That one swap usually pays for itself.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Generate Open Graph Images On the Fly With Satori and Resvg</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Tue, 16 Jun 2026 01:41:36 +0000</pubDate>
      <link>https://dev.to/raxxostudios/generate-open-graph-images-on-the-fly-with-satori-and-resvg-3mke</link>
      <guid>https://dev.to/raxxostudios/generate-open-graph-images-on-the-fly-with-satori-and-resvg-3mke</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Satori turns JSX into SVG with zero browser, runs in 40ms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resvg renders SVG to PNG in pure Rust, no Puppeteer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full edge function code that caches per-page cards&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each blog post gets a unique share image automatically&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every link I shared on social used to show the same generic banner. Now every page on raxxo.shop generates its own Open Graph card with the title, the author, and a clean layout, all rendered on the fly. No headless browser, no screenshot service, no build step. Here is the entire pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Skip the Headless Browser
&lt;/h2&gt;

&lt;p&gt;The standard way to make share cards is Puppeteer. You spin up a headless Chromium, load an HTML page, screenshot it, save the PNG. It works. It is also slow, heavy, and a pain to run on an edge function.&lt;/p&gt;

&lt;p&gt;Chromium is roughly 280MB unpacked. Most edge runtimes cap your bundle at 50MB or refuse to launch a browser at all. Cold starts run 800ms to 2 seconds because the browser has to boot before it draws a single pixel. I ran a Puppeteer card service for a while and the p95 latency sat around 1.4 seconds. For an image that loads in the background of a social preview, that is fine until you have 60 posts and a crawler hits them all at once.&lt;/p&gt;

&lt;p&gt;The alternative is Satori plus Resvg. Satori is a library that takes JSX (or a plain object tree) and produces an SVG string. It does the layout in JavaScript using a flexbox engine, so no browser, no DOM. Resvg then takes that SVG and rasterizes it to PNG using Rust compiled to WebAssembly. Both run inside a normal serverless or edge function.&lt;/p&gt;

&lt;p&gt;The numbers that sold me: Satori renders a typical 1200x630 card in about 40ms. Resvg converts the SVG to PNG in another 30 to 60ms depending on font count. Total cold path under 200ms, warm path closer to 90ms. The whole bundle is under 8MB instead of 280MB.&lt;/p&gt;

&lt;p&gt;There is a tradeoff. Satori only supports a subset of CSS. Flexbox works. Grid does not. No CSS animations, no filters beyond a few, no external stylesheets. You build cards with flex containers, fixed positions, colors, and text. For a share card that is exactly the vocabulary you need anyway, so the limit rarely bites.&lt;/p&gt;

&lt;p&gt;If you want the broader pattern of doing real work at the edge instead of on a fat server, the same thinking shows up in how I structure the rest of the stack. Background: &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; goes through how I keep services small and composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Minimal Satori Setup
&lt;/h2&gt;

&lt;p&gt;Install two packages: &lt;code&gt;satori&lt;/code&gt; and &lt;code&gt;@resvg/resvg-js&lt;/code&gt;. You also need at least one font as a buffer, because Satori cannot guess a typeface the way a browser falls back to system fonts.&lt;/p&gt;

&lt;p&gt;Here is the core function. I am using the object form instead of real JSX so it runs without a JSX transform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;satori&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;satori&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resvg&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="s1"&gt;@resvg/resvg-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fontData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./fonts/Inter-Bold.ttf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;satori&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1200px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;630px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;flexDirection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;justifyContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;space-between&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;64px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0b0b0f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;28px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#9aa0ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;72px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lineHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;32px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#8a8a93&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;raxxo.shop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fontData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resvg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fitTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;png&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;asPng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole thing. Satori returns an SVG string. Resvg takes it, renders, and &lt;code&gt;asPng()&lt;/code&gt; hands back a buffer of bytes you can return as &lt;code&gt;image/png&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few details that cost me time. Every &lt;code&gt;div&lt;/code&gt; with more than one child needs &lt;code&gt;display: flex&lt;/code&gt; set explicitly, or Satori throws. It does not assume block layout. Fonts must be &lt;code&gt;.ttf&lt;/code&gt; or &lt;code&gt;.otf&lt;/code&gt;, not &lt;code&gt;.woff2&lt;/code&gt;, because the SVG renderer cannot decompress woff2. If your title overflows, set a &lt;code&gt;maxWidth&lt;/code&gt; and Satori wraps it for you, but only if &lt;code&gt;display: flex&lt;/code&gt; and &lt;code&gt;flexWrap&lt;/code&gt; or a fixed width is present.&lt;/p&gt;

&lt;p&gt;For 1200x630, the magic numbers are 64px padding and a 72px headline. That sizing reads cleanly when a platform shrinks the preview to a thumbnail. Smaller text turns to mush at thumbnail scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serving It From an Edge Function
&lt;/h2&gt;

&lt;p&gt;The render function is half the job. The other half is wiring it to a URL so `` can point at it.&lt;/p&gt;

&lt;p&gt;I expose a route like &lt;code&gt;/og?title=...&amp;amp;tag=...&lt;/code&gt;. The function reads the query params, calls &lt;code&gt;renderCard&lt;/code&gt;, and returns the PNG with cache headers. The cache headers matter more than the render speed, because once a card is generated the crawler should never trigger a fresh render again.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;javascript&lt;/p&gt;

&lt;p&gt;export default async function handler(req) {&lt;br&gt;
  const url = new URL(req.url)&lt;br&gt;
  const title = url.searchParams.get('title') ?? 'Untitled'&lt;br&gt;
  const tag = url.searchParams.get('tag') ?? 'Lab'&lt;/p&gt;

&lt;p&gt;const png = await renderCard({ title, tag })&lt;/p&gt;

&lt;p&gt;return new Response(png, {&lt;br&gt;
    headers: {&lt;br&gt;
      'content-type': 'image/png',&lt;br&gt;
      'cache-control': 'public, immutable, max-age=31536000',&lt;br&gt;
    },&lt;br&gt;
  })&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;immutable, max-age=31536000&lt;/code&gt; line tells the CDN to hold the result for a year. Because the title and tag are in the URL, any change to a post title produces a new URL and a new cache key automatically. No manual invalidation.&lt;/p&gt;

&lt;p&gt;One trap: passing raw user text in a query string breaks on special characters. I &lt;code&gt;encodeURIComponent&lt;/code&gt; the title when I build the meta tag, and the function decodes it through &lt;code&gt;URLSearchParams&lt;/code&gt; for free. Long titles also need a length cap, around 90 characters, or the layout breaks. I truncate with an ellipsis before passing it in.&lt;/p&gt;

&lt;p&gt;I load the font once at module scope, outside the handler, so it stays warm between invocations on the same instance. Reading a 350KB font file on every request added 15ms I did not need. Hoisting it out cut warm latency by that amount across thousands of calls.&lt;/p&gt;

&lt;p&gt;For social scheduling I push the generated card URLs straight into &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; so each scheduled post pulls its own preview. If you generate the images first and let the platform crawl them once, the queue stays fast and nothing renders twice. The whole point is that the heavy work happens at the edge near the user, not on a single origin server that becomes a queue when a crawler fans out across your sitemap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates, Fonts, and the Edge Cases
&lt;/h2&gt;

&lt;p&gt;A single layout gets boring after a dozen posts. I keep three template functions: one for tutorials, one for case studies, one default. The route picks one based on a &lt;code&gt;template&lt;/code&gt; param. Each is just a different object tree, maybe a different accent color and a small icon.&lt;/p&gt;

&lt;p&gt;Emoji is the first thing that breaks. Satori does not ship an emoji font, so a 🚀 renders as a blank box. The fix is to pass a &lt;code&gt;loadAdditionalAsset&lt;/code&gt; callback that fetches the right glyph from an emoji CDN on demand. I cache those fetches in a Map keyed by code point, so the same emoji never downloads twice within a warm instance. With 12 common emoji preloaded, the extra latency dropped to near zero.&lt;/p&gt;

&lt;p&gt;Multiple font weights matter more than I expected. A card with one weight looks flat. I load Inter at 400, 600, and 700 and assign weights per element. The bundle grows by about 700KB total, which is nothing next to a browser. Mixing a regular tag label against a bold headline gives the card a real visual hierarchy that reads at thumbnail size.&lt;/p&gt;

&lt;p&gt;Images inside cards are the trickiest part. Satori accepts an `&lt;code&gt; with a &lt;/code&gt;src`, but the src must be a base64 data URI or a fully resolvable URL, and Resvg has to fetch and decode it during rasterization. A remote PNG logo adds 40 to 120ms. I inline my logo as a base64 string baked into the template, so there is no network hop at render time.&lt;/p&gt;

&lt;p&gt;Color and contrast deserve a test pass. I render every template once and check it on a phone-sized preview, because what looks crisp at 1200px wide can vanish at 300px. Light gray subtitles below &lt;code&gt;#888&lt;/code&gt; disappear against dark backgrounds when compressed by the platform.&lt;/p&gt;

&lt;p&gt;For the deeper version of how I keep these template files small and reusable, I covered the same modular approach in &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;. The principle carries: small typed functions, one job each, no clever inheritance. A card template is 30 lines. When I want a fourth style I copy one, change the color tokens, and ship it the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Satori plus Resvg replaced a 280MB browser with an 8MB pipeline that renders a share card in under 200ms cold and 90ms warm. JSX goes in, SVG comes out, PNG bytes come back, and a one-year cache header means each unique title renders exactly once before the CDN takes over. No screenshot service, no Chromium, no build step.&lt;/p&gt;

&lt;p&gt;The setup that took me the longest was not the render code. It was the font handling, the emoji callback, and the cache headers, which is where the real latency lives. Get those three right and the rest is just designing object trees that happen to look like flexbox.&lt;/p&gt;

&lt;p&gt;If you run a content site or a store with lots of pages, this is the highest-use afternoon project I know of. Every link you share starts looking deliberate instead of generic. Start with one template, wire the route, point your meta tag at it, and add styles as you go. The code above is the whole skeleton. Copy it, swap the font, and you have unique cards by tonight.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Native HTML Dialog Element: 6 Modal Patterns I Ship Without a Library</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:20:42 +0000</pubDate>
      <link>https://dev.to/raxxostudios/the-native-html-dialog-element-6-modal-patterns-i-ship-without-a-library-3cmj</link>
      <guid>https://dev.to/raxxostudios/the-native-html-dialog-element-6-modal-patterns-i-ship-without-a-library-3cmj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Native dialog removed a 14KB modal library from 9 sites&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;method=dialog forms close and return values with zero JS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;@starting-style plus closedby="any" handle exit animation and light-dismiss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One case (nested non-modal popovers) still needs a small helper&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deleted a 14KB modal library from 9 of my sites last quarter. The native `` element replaced every feature I was paying for: focus trap, top-layer stacking, escape-to-close, backdrop styling. Here are the 6 patterns I ship now, plus the one situation where I still reach for help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Free Focus Trap And Top-Layer Stacking
&lt;/h2&gt;

&lt;p&gt;The reason most people install a modal library is the focus trap. When a dialog opens, keyboard focus needs to stay inside it. Tab past the last button and you should loop back to the first, not fall through to the page behind. Writing that by hand is a pain. You track focusable elements, listen for Tab and Shift+Tab, and handle the wrap. Libraries charge you kilobytes for it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dialog.showModal()&lt;/code&gt; does the trap for free. Call it and the browser confines Tab to the dialog, sends Escape to close, and marks everything behind it inert so screen readers skip it. I tested this with VoiceOver on 3 of my product pages and the reading order was correct without a single ARIA attribute from me.&lt;/p&gt;

&lt;p&gt;The second free thing is the top layer. When you open a modal dialog, the browser promotes it above every other element regardless of z-index. I used to fight stacking context bugs constantly, where a modal would sit behind a sticky header because some ancestor had &lt;code&gt;transform&lt;/code&gt; set. The top layer ignores all of that. A modal dialog paints on top, period.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;javascript&lt;/p&gt;

&lt;p&gt;Close&lt;/p&gt;

&lt;h2&gt;
  
  
  Your cart
&lt;/h2&gt;

&lt;p&gt;document.querySelector('#cart').showModal();&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;autofocus&lt;/code&gt; attribute. The browser moves initial focus to it when the dialog opens, which is the accessible default you want. Use &lt;code&gt;show()&lt;/code&gt; instead of &lt;code&gt;showModal()&lt;/code&gt; and you get a non-modal dialog: no backdrop, no focus trap, no inert background. I use the modal version for anything that demands a decision (checkout confirm, age gate) and the non-modal version for things that should not block the page. If you came up through the era where this all needed JavaScript, my notes on &lt;a href="https://dev.to/blogs/lab/5-css-animations-that-needed-javascript-until-2026"&gt;5 CSS Animations That Needed JavaScript Until 2026&lt;/a&gt; cover the same shift in mindset.&lt;/p&gt;

&lt;p&gt;That is two of the three big reasons people install a library, gone, with two method calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling ::backdrop And The method=dialog Form Trick
&lt;/h2&gt;

&lt;p&gt;The dimmed overlay behind a modal has its own pseudo-element: &lt;code&gt;::backdrop&lt;/code&gt;. You style it like any selector. No extra div, no positioned overlay that you have to z-index above the content.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;css&lt;/p&gt;

&lt;p&gt;dialog::backdrop {&lt;br&gt;
  background: rgb(0 0 0 / 0.6);&lt;br&gt;
  backdrop-filter: blur(4px);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;I run a 4px blur on the backdrop across all my storefronts. It costs one line and makes the modal feel intentional instead of bolted on. You can animate &lt;code&gt;::backdrop&lt;/code&gt; too, which matters for the exit animation pattern below.&lt;/p&gt;

&lt;p&gt;The form trick is the part people miss. A &lt;code&gt;&lt;br&gt;
&lt;/code&gt; inside a dialog closes it on submit and records which button was used, all without JavaScript.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;Delete this draft?&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cancel
Delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;When the user clicks Delete, the dialog closes and &lt;code&gt;dialog.returnValue&lt;/code&gt; becomes "delete". You read it in a single &lt;code&gt;close&lt;/code&gt; event listener:&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;javascript&lt;/p&gt;

&lt;p&gt;confirm.addEventListener('close', () =&amp;gt; {&lt;br&gt;
  if (confirm.returnValue === 'delete') removeDraft();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;That is a complete confirm dialog with two outcomes and almost no script. I shipped exactly this for a destructive action on one of my &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; admin tools and it replaced about 40 lines of state handling. If you build on &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt;, this pattern slots right into theme app blocks because it needs nothing global.&lt;/p&gt;

&lt;p&gt;The catch worth knowing: &lt;code&gt;method="dialog"&lt;/code&gt; only closes the dialog, it does not submit data anywhere. If you need a real network submit, keep a normal form and call &lt;code&gt;dialog.close()&lt;/code&gt; yourself after the fetch resolves. I mixed the two up early on and spent 20 minutes confused why my POST never fired. The fix was one attribute. For the broader pattern of moving logic out of JavaScript and into the platform, &lt;a href="https://dev.to/blogs/lab/css-has-in-production-6-selectors-that-replaced-javascript-across-my-sites"&gt;CSS :has() in Production&lt;/a&gt; walks through the same trade-offs on the selector side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Light-Dismiss With closedby And Clean Exit Animations
&lt;/h2&gt;

&lt;p&gt;For years the standard request was "let me click outside the modal to close it." That meant a click listener on the backdrop, math to check the click target, and an edge case where clicking inside and dragging out would close it by accident. Now there is an attribute.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;plaintext&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;closedby="any"&lt;/code&gt; gives you light-dismiss: click the backdrop or press Escape and the dialog closes. &lt;code&gt;closedby="closerequest"&lt;/code&gt; allows only Escape and the platform close gesture. &lt;code&gt;closedby="none"&lt;/code&gt; blocks both, which I use for a payment-in-progress modal that must not be dismissed mid-transaction. Three values, zero listeners. This shipped to stable Chrome and is rolling through the other engines, so I feature-detect and fall back to a tiny backdrop-click handler where it is missing.&lt;/p&gt;

&lt;p&gt;Exit animations were the other thing libraries handled. The problem: when you set &lt;code&gt;display: none&lt;/code&gt; the element vanishes instantly, so there is nothing to animate out. The new combo is &lt;code&gt;@starting-style&lt;/code&gt; for the entry and &lt;code&gt;transition-behavior: allow-discrete&lt;/code&gt; for the exit.&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;css&lt;/p&gt;

&lt;p&gt;dialog {&lt;br&gt;
  opacity: 1;&lt;br&gt;
  transition: opacity 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;&lt;br&gt;
}&lt;br&gt;
dialog:not([open]) { opacity: 0; }&lt;br&gt;
@starting-style {&lt;br&gt;
  dialog[open] { opacity: 0; }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@starting-style&lt;/code&gt; defines the values the dialog starts from before it opens, so it fades in. &lt;code&gt;allow-discrete&lt;/code&gt; on &lt;code&gt;display&lt;/code&gt; and &lt;code&gt;overlay&lt;/code&gt; keeps the element painted through the exit transition instead of cutting it the instant the attribute drops. The &lt;code&gt;overlay&lt;/code&gt; property is what keeps it in the top layer until the fade finishes. I run a 200ms fade plus an 8px translate on every dialog now and it reads as polished without a single requestAnimationFrame call.&lt;/p&gt;

&lt;p&gt;This is the same family of platform animation work I covered in &lt;a href="https://dev.to/blogs/lab/css-scroll-driven-animations-6-patterns-i-ship-in-2026"&gt;CSS Scroll-Driven Animations: 6 Patterns I Ship in 2026&lt;/a&gt;, and the discrete-transition trick shows up there too. If you want the whole animation story without JS, &lt;a href="https://dev.to/blogs/lab/view-transitions-api-5-patterns-i-use-across-raxxo-sites-in-2026"&gt;View Transitions API: 5 Patterns&lt;/a&gt; pairs cleanly with dialogs for full-screen route changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Case Where A Library Still Wins
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the limit, because pretending native covers everything would cost you a debugging afternoon. The element handles single modals beautifully. It struggles with deeply nested, non-modal stacks where multiple floating panels need to coexist and dismiss in a specific order.&lt;/p&gt;

&lt;p&gt;Picture a non-modal dialog that contains a select, and that select opens a custom dropdown, and the dropdown has a tooltip on one option. With &lt;code&gt;show()&lt;/code&gt; (non-modal), there is no focus trap and no inert background, so you are back to managing focus order and outside-click dismissal across three independent layers. The native top layer stacks them, but it does not orchestrate which one Escape should close first or how focus should return down the chain. That orchestration is exactly what a small library like a focus-management helper gives you, and it is worth the 4KB in that narrow case.&lt;/p&gt;

&lt;p&gt;I hit this building a filter panel for a catalog with 1,200 SKUs. The panel was non-modal so shoppers could keep scrolling the grid, but it held nested expandable groups with their own popovers. Native dialog plus the popover API got me 90% there. The last 10%, the predictable Escape order across nested popovers, was flaky enough that I added a 3KB helper just for focus return. Everything else stayed native.&lt;/p&gt;

&lt;p&gt;The honest rule I use: if it is one modal at a time, native wins outright and I write zero JavaScript for behavior. If I have two or more interactive floating layers that must coordinate dismissal and focus, I keep a tiny helper and let it manage only that coordination, not the rendering.&lt;/p&gt;

&lt;p&gt;A few smaller gotchas I logged so you do not repeat them. A dialog with no &lt;code&gt;autofocus&lt;/code&gt; and no focusable child puts focus on the dialog itself, which is fine but means Escape works and Tab does nothing visible, confusing in testing. Setting &lt;code&gt;max-height&lt;/code&gt; matters because long dialogs can overflow the viewport with no scroll. And the default &lt;code&gt;dialog&lt;/code&gt; has a centered position via margin auto that you will likely override. For a related layout problem, anchoring a small panel to a trigger, &lt;a href="https://dev.to/blogs/lab/css-anchor-positioning-is-production-ready-5-patterns"&gt;CSS Anchor Positioning Is Production-Ready&lt;/a&gt; is the piece I reach for instead of a dialog entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;The native `` element removed a dependency I had carried for years. Focus trap, top-layer stacking, backdrop styling, escape-to-close, light-dismiss, return values, and exit animations all live in the platform now. I ship 5 of the 6 patterns with zero behavior JavaScript and reach for a 3KB helper only when multiple floating layers must coordinate.&lt;/p&gt;

&lt;p&gt;If you are still loading a modal library, open one component and try the swap. Replace the open call with &lt;code&gt;showModal()&lt;/code&gt;, delete your focus-trap code, add a &lt;code&gt;::backdrop&lt;/code&gt; rule, and wire one &lt;code&gt;close&lt;/code&gt; listener. On my sites that swap took an afternoon per component and cut real bytes off every page that rendered a modal.&lt;/p&gt;

&lt;p&gt;I document each of these platform-over-library calls as I make them, because cutting dependencies is most of how a one-person studio stays fast. If you want the system I use to decide what to build, what to delete, and how I keep it consistent across repos, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; lays out the whole workflow. Start with the dialog swap, measure the bytes you drop, and keep the helper only where the platform genuinely runs out.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>The Solo Studio Software Stack: What I Actually Pay For in 2026</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:20:05 +0000</pubDate>
      <link>https://dev.to/raxxostudios/the-solo-studio-software-stack-what-i-actually-pay-for-in-2026-3lfl</link>
      <guid>https://dev.to/raxxostudios/the-solo-studio-software-stack-what-i-actually-pay-for-in-2026-3lfl</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Total stack runs 312 EUR/month across 11 tools&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cut 4 subscriptions worth 178 EUR by consolidating&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One swap paid for itself in 6 days&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rule: any tool under 20 EUR earns its keep, anything over needs proof&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My software stack costs 312 EUR a month. Last year it was 490 EUR. I did not get cheaper tools, I got fewer of them, and the ones that stayed all do measurable work. Here is the exact line-item breakdown of what a one-person studio actually pays for in 2026, what I killed, and the single swap that paid for itself in under a week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Four That Never Get Questioned
&lt;/h2&gt;

&lt;p&gt;Some tools are not up for debate. They run the business, and cutting them would cost me more in lost time than the subscription ever could.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; is first at 32 EUR a month on the Basic plan. Every product I sell lives there. The storefront, the checkout, the order pipeline, all of it. I tried moving to a cheaper static-site-plus-payment-link setup in early 2025 and lasted eleven days before I crawled back. The hidden cost of DIY checkout is every edge case you did not plan for: failed payments, address validation, tax on digital goods. &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; eats those for 32 EUR and I sleep fine.&lt;/p&gt;

&lt;p&gt;Claude is second at 90 EUR a month for the Max-tier access I run. This is the single biggest line item and also the easiest to justify. It drafts, it codes, it audits my own work, it writes first passes of nearly everything. If you want the full picture of how I wire it into actual production, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the setup end to end.&lt;/p&gt;

&lt;p&gt;Third is my domain and email through a registrar plus a privacy-focused mail host, combined at 14 EUR a month. Boring, essential, never touched.&lt;/p&gt;

&lt;p&gt;Fourth is cloud storage and backup at 11 EUR a month. Two copies, two providers, automated nightly. The day a drive dies, this 11 EUR becomes the best money I ever spent. I have had exactly one drive die since 2023 and the restore took 40 minutes instead of a panic spiral.&lt;/p&gt;

&lt;p&gt;That is 147 EUR for the four things that, if they vanished tomorrow, would stop the studio dead. Everything below this line is optional in theory. In practice some of it earns more than the core. I rank tools not by what they cost but by what breaks when I remove them. The core four break everything. The next tier breaks output speed, which is its own kind of expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Creative Pipeline: Where the Money Actually Works
&lt;/h2&gt;

&lt;p&gt;This is where a studio either spends smart or bleeds. Image and audio generation tools are easy to over-buy because every launch promises to replace three others.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; holds the top spot here at 39 EUR a month. It upscales and refines the images that go into product mockups and blog headers. I ran a hard test on it for 30 days before committing, and the full writeup is in &lt;a href="https://dev.to/blogs/lab/30-days-with-the-magnific-image-pipeline-what-stuck-and-what-got-killed"&gt;30 Days With the Magnific Image Pipeline&lt;/a&gt;. Short version: it stayed because the output quality cleared a bar that three free alternatives could not, and the difference shows on product pages where image quality is the whole sell.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://try.elevenlabs.io/8pbaehnkoq4u" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt; is next at 22 EUR a month. Voiceover for short videos, audio previews, the occasional narration. I used to outsource this per project and the math was brutal. One 90-second voiceover from a freelancer ran 40 to 60 EUR and took two days. Now I generate, review, and ship the same day for a flat 22 EUR a month no matter how many I make. In a month where I produced eleven audio clips, the per-clip cost dropped to under 2 EUR. That is the kind of swap that does not need a spreadsheet to justify.&lt;/p&gt;

&lt;p&gt;A generic stock asset subscription rounds out this tier at 16 EUR a month. Fonts, textures, the occasional video clip. I considered cutting it twice and both times found myself needing exactly one asset that would have cost more as a one-off purchase than the whole month.&lt;/p&gt;

&lt;p&gt;That is 77 EUR for the creative engine. Combined with the core four, I am at 224 EUR and every euro is doing visible work. The rule I apply: anything under 20 EUR a month gets a long leash because the time it saves almost always beats the cost. Anything over 20 EUR has to prove itself on output I can point to. Magnific and ElevenLabs both clear that bar. The stock subscription squeaks by on convenience, and I review it every quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boring Money Tools That Quietly Pay for Themselves
&lt;/h2&gt;

&lt;p&gt;Nobody gets excited about accounting software. But the back-office stack is where I cut the most waste, and ironically where the swap that paid for itself in six days happened.&lt;/p&gt;

&lt;p&gt;Stripe runs my payments and tax handling. There is no flat monthly fee, it takes a slice per transaction, so it does not appear as a line item but it is core. The reason I keep it over alternatives is the tax automation, which I broke down fully in &lt;a href="https://dev.to/blogs/lab/solo-studio-invoicing-in-2026-stripe-tax-datev-export-ust-va-via-api"&gt;Solo Studio Invoicing in 2026&lt;/a&gt;. That setup alone saves me hours every filing period.&lt;/p&gt;

&lt;p&gt;My bookkeeping tool sits at 19 EUR a month. It syncs bank feeds, categorizes transactions, and exports clean reports. Here is the swap. I used to pay 49 EUR a month for a heavier accounting suite that did ten things I never used. I moved to the lighter 19 EUR tool, and because it exports cleanly into the format my routine needs, my monthly close went from a half-day slog to 90 minutes. The 30 EUR I saved per month was nice. The time was the real win. That whole routine lives in &lt;a href="https://dev.to/blogs/lab/solo-studio-bookkeeping-in-90-minutes-a-month-my-stack-and-routine"&gt;Solo Studio Bookkeeping in 90 Minutes a Month&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why did it pay for itself in six days? The first close on the new tool surfaced two duplicate charges I had been paying for months, one of them a dead subscription. Catching those covered the switching effort almost immediately, and then kept paying every month after.&lt;/p&gt;

&lt;p&gt;A password manager rounds this tier out at 4 EUR a month. Trivial cost, enormous downside if skipped. One studio I know lost access to a client account for a week over a forgotten login. Four euros buys me out of that entire category of problem.&lt;/p&gt;

&lt;p&gt;There is also the VAT side of buying SaaS from EU vendors, which has a quieter cost than most people realize. If you buy tools across borders, &lt;a href="https://dev.to/blogs/lab/eu-reverse-charge-vat-for-solo-saas-buyers-the-zero-euro-math-most-devs-miss"&gt;EU Reverse-Charge VAT for Solo SaaS Buyers&lt;/a&gt; explains the zero-euro math that most developers miss entirely. Knowing it changed how I evaluate vendor pricing.&lt;/p&gt;

&lt;p&gt;Back-office total: 23 EUR in flat fees plus Stripe's per-transaction cut. The cheapest tier and arguably the highest use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Got Cut and the Rules I Use to Decide
&lt;/h2&gt;

&lt;p&gt;I killed four subscriptions in the last year worth 178 EUR a month combined. Here is what went and why.&lt;/p&gt;

&lt;p&gt;The first was a project management app at 24 EUR a month. As a one-person studio I was building elaborate boards that only I would ever read. A plain text file and a calendar do the same job for free. Gone.&lt;/p&gt;

&lt;p&gt;The second was a dedicated social scheduling tool I overpaid for at 65 EUR a month on a tier built for teams. I moved my scheduling to &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; on a plan that fits an actual solo operator, and the savings were immediate. I do not need ten team seats. I need to queue posts and walk away.&lt;/p&gt;

&lt;p&gt;The third was an AI writing tool at 45 EUR a month that I bought before Claude could do the same work better. Once the core model handled drafting, the specialized writer became dead weight. Cut.&lt;/p&gt;

&lt;p&gt;The fourth was a video editing suite at 44 EUR a month that I used twice in a quarter. I switched to a one-time-purchase editor and stopped paying rent on software I barely opened.&lt;/p&gt;

&lt;p&gt;The rules that govern all of this are simple and I apply them every quarter:&lt;/p&gt;

&lt;p&gt;One, if I cannot name the specific output a tool produced this month, it is on probation. Two opens-and-forgets in a row and it is gone.&lt;/p&gt;

&lt;p&gt;Two, flat fees under 20 EUR get a long leash because the downside of not having them usually beats the cost. The password manager and bookkeeping tool live here.&lt;/p&gt;

&lt;p&gt;Three, anything over 20 EUR has to survive a real test, not a free trial I forget about. Magnific got a 30-day trial-by-fire. ElevenLabs proved itself against freelance pricing in week one.&lt;/p&gt;

&lt;p&gt;Four, never buy a team tier as a team of one. I paid that tax twice and it stung both times.&lt;/p&gt;

&lt;p&gt;Five, one tool replacing two is always worth the migration friction, even if the migration is annoying. Consolidation compounds. Fewer logins, fewer renewals, fewer surprise charges. Pricing discipline applies to my own products too, which I wrote about in &lt;a href="https://dev.to/blogs/lab/why-i-doubled-my-product-prices-and-lost-zero-customers"&gt;Why I Doubled My Product Prices and Lost Zero Customers&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;The full stack lands at 312 EUR a month: 147 EUR core, 77 EUR creative, 23 EUR back-office flat fees, plus Stripe's per-transaction slice and the Buffer scheduling plan. A year ago it was 490 EUR and I was getting less done. The lesson was not to chase the cheapest option in every category. It was to know exactly what each tool produces and to cut anything I could not point to.&lt;/p&gt;

&lt;p&gt;If you run a solo studio, do the audit. Open your last bank statement, find every recurring charge, and ask one question per line: what did this make this month? You will find a dead subscription. Almost everyone does. Mine cost me months before I caught it.&lt;/p&gt;

&lt;p&gt;The tools that stayed are the ones doing visible work, and most of them link out across the back-office posts above if you want the deeper setup. The &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; is where I show how the AI piece ties the whole stack together, which is the part that punches well above its 90 EUR weight.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Turn One Blog Post Into a LinkedIn Post, an X Thread, and an IG Carousel</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:19:30 +0000</pubDate>
      <link>https://dev.to/raxxostudios/turn-one-blog-post-into-a-linkedin-post-an-x-thread-and-an-ig-carousel-39mp</link>
      <guid>https://dev.to/raxxostudios/turn-one-blog-post-into-a-linkedin-post-an-x-thread-and-an-ig-carousel-39mp</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pull the spine first: one argument, three reshapes, never cross-post&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LinkedIn wants the lesson, X wants the build, IG wants the proof&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each platform gets a different hook from the same source paragraph&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Schedule the whole week in one 40-minute block, then walk away&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One blog post turned into a LinkedIn post, an X thread, and an Instagram carousel pulled 4,100 views across three feeds last week. I wrote the article once. The three social pieces took 40 minutes total. Here is the exact flow I use, and why cross-posting the same text everywhere fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Cross-Posting, Start Reshaping
&lt;/h2&gt;

&lt;p&gt;The mistake almost everyone makes is copy-paste. They write a blog post, grab the intro, drop it on LinkedIn, paste the same lines into X, and slap a screenshot on Instagram. Then they wonder why each one gets 12 likes.&lt;/p&gt;

&lt;p&gt;The reason is simple. Each platform rewards a different shape of the same idea. LinkedIn readers scroll for a takeaway they can repeat in a meeting. X readers want to watch you think out loud, step by step. Instagram readers want a visual they can swipe through in eight seconds without reading much at all. Same argument, three completely different containers.&lt;/p&gt;

&lt;p&gt;So the first move is not writing. It is extraction. I open the blog post and find the spine, which is the single sentence that the whole article is trying to prove. Not the topic, the claim. If the article is "SEO for Indie Creators," the topic is SEO but the spine might be "ranking pages beat viral posts because they earn for two years." That spine is the only thing all three platform pieces share.&lt;/p&gt;

&lt;p&gt;I write that spine at the top of a blank note. Then I list the three or four supporting beats underneath it, the ones the article uses to make the case. For a 1,800 word post that is usually four beats. These beats become my raw material. I do not reuse sentences. I reuse the logic.&lt;/p&gt;

&lt;p&gt;This is the part that separates a repurpose flow from a spam flow. When you reshape instead of copy, the LinkedIn version and the X version can both link back to the full article without feeling redundant, because neither one is the article. They are two arguments for reading it.&lt;/p&gt;

&lt;p&gt;I learned this the hard way after months of cross-posting identical blocks and watching engagement flatline. The fix was not better writing. It was accepting that one piece of content is actually a source file, not a finished product. If you want the video version of this same idea, &lt;a href="https://dev.to/blogs/lab/how-to-repurpose-one-video-into-10-pieces-of-content-with-ai"&gt;How to Repurpose One Video into 10 Pieces of Content with AI&lt;/a&gt; covers the equivalent flow for footage instead of text. Different inputs, same principle: extract the spine, reshape per channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LinkedIn Version: Lead With the Lesson
&lt;/h2&gt;

&lt;p&gt;LinkedIn rewards the takeaway up front. Nobody on LinkedIn is reading to be surprised at the end. They are scanning for a line worth saving.&lt;/p&gt;

&lt;p&gt;So my LinkedIn version always opens with the spine, rewritten as a flat statement. From the SEO example: "Ranking pages earn for two years. Viral posts earn for two days. I keep choosing the slower one." That is line one. No throat-clearing, no "I've been thinking a lot about."&lt;/p&gt;

&lt;p&gt;Then I structure the body as a short story or a list of three. LinkedIn formatting matters more than the words. One sentence per line. White space between every thought. The feed truncates around three lines, so the first three lines have to earn the "see more" click. I rewrite those three lines four or five times before I move on, because they do 80 percent of the work.&lt;/p&gt;

&lt;p&gt;The middle is two of my four beats from the spine note, written as lessons I learned rather than facts I know. "I shipped 30 viral attempts before I understood this" beats "viral content is unreliable." Specifics and numbers. I pull real numbers from the article wherever I can. A LinkedIn post with one concrete figure outperforms a post full of advice.&lt;/p&gt;

&lt;p&gt;The close is a single question. Not "what do you think," which gets nothing, but a specific fork. "Are you building for next week or next year?" That gives people something to answer in one word.&lt;/p&gt;

&lt;p&gt;I drop the blog link in the first comment, not the post body, because LinkedIn suppresses posts with outbound links. The comment trick keeps reach while still routing traffic. I have watched the same post get triple the views when the link sits in comments instead of the caption.&lt;/p&gt;

&lt;p&gt;The whole LinkedIn version runs maybe 180 words. It took longer to write than the X thread because LinkedIn is unforgiving about flabby lines. Every word has to defend its spot. If you want the reasoning behind shipping in batches instead of daily, &lt;a href="https://dev.to/blogs/lab/why-i-ship-blog-articles-in-clusters-of-3-not-daily"&gt;Why I Ship Blog Articles in Clusters of 3&lt;/a&gt; explains the cadence I build all of this around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The X Thread: Show the Build, Not the Bow
&lt;/h2&gt;

&lt;p&gt;X is the opposite of LinkedIn. LinkedIn wants the conclusion. X wants the process, the receipts, the "here is exactly how" walkthrough that reads like you are figuring it out in real time.&lt;/p&gt;

&lt;p&gt;So my X thread takes the same spine but reorders it. The hook tweet is a result or a tension, never the lesson. "I posted the same blog to 3 platforms. One got 4,100 views. The other two got 30 combined. Here's what I changed." That sets up a thread people stay for because they want the resolution.&lt;/p&gt;

&lt;p&gt;Then each beat from my spine note becomes one tweet. I keep tweets to two or three short lines. The thread is the only format where I can be granular, so I add the steps the blog post had to skip for length. Numbers, tool names, the exact order I did things. X readers reward specificity harder than any other platform. A vague thread dies in the first two tweets.&lt;/p&gt;

&lt;p&gt;I cap most threads at six or seven tweets. Past that, the drop-off gets brutal. The last tweet always restates the spine as a one-liner and links the full article, because by then the reader trusts me enough to click. On X the link can go in the final tweet without much penalty, unlike LinkedIn.&lt;/p&gt;

&lt;p&gt;The reshaping work here is turning declarative blog prose into conversational fragments. The blog says "I tested four scheduling tools." The thread says "Tested 4 schedulers. Three were bloated. One did the one thing I needed." Shorter, punchier, more opinionated. X tolerates and rewards a sharper voice than LinkedIn does.&lt;/p&gt;

&lt;p&gt;For scheduling all of this, I run the queue through &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; so the thread, the LinkedIn post, and the carousel all go out on staggered days from one dashboard. I do not post live. Posting live means I am at the mercy of whatever mood I am in. Queuing means the week ships whether I feel like it or not. My full automation stack lives in &lt;a href="https://dev.to/blogs/lab/i-automated-90-percent-of-my-content-pipeline-with-4-tools"&gt;I Automated 90 Percent of My Content Pipeline With 4 Tools&lt;/a&gt; if you want the rest of the rig.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Instagram Carousel: Make the Argument Visual
&lt;/h2&gt;

&lt;p&gt;Instagram is the platform where the words almost stop mattering. A carousel lives or dies on slide one, the cover, which has to stop the swipe with a single bold line. Everything after that is momentum.&lt;/p&gt;

&lt;p&gt;I take the spine and turn it into a six-to-eight slide arc. Slide one is the hook as a big statement: "1 blog post = 3 platforms = 0 extra writing." Slides two through six are one beat each, one idea per slide, almost no body text. Maybe a headline and one supporting line. The carousel is a slideshow, not an essay, and people swipe at speed.&lt;/p&gt;

&lt;p&gt;The design has to be consistent. Same font, same two colors, same layout grid on every slide so it reads as one set. I build the slide backgrounds and any supporting visuals in &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; when I need clean upscaled imagery that holds up at full resolution in the feed. Blurry slides get skipped. Sharp slides get saved, and saves are what Instagram actually rewards.&lt;/p&gt;

&lt;p&gt;The last slide is the call to action, but soft. "Full breakdown on the blog, link in bio." Instagram punishes outbound links worse than any platform, so the carousel is a top-of-funnel grab. I am not expecting clicks. I am expecting saves and a couple of follows from people who liked the format.&lt;/p&gt;

&lt;p&gt;The reshape here is the hardest, because text-heavy blog logic has to compress into eight visual beats. I cut ruthlessly. If a beat needs three sentences to land, it does not belong in a carousel, it belongs in the X thread. The carousel only carries ideas that survive being reduced to one line plus a graphic.&lt;/p&gt;

&lt;p&gt;The caption gets one job: repeat the cover hook and ask for the save. "Save this for your next repurpose session." That single instruction lifts saves more than any clever caption. The visual already did the persuading. The caption just tells people what to do with it. The full deck takes about 15 minutes once the spine note exists, because I am arranging, not inventing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;One blog post is not one piece of content. It is a source file. Pull the spine, the single claim the whole article proves, then reshape it three ways instead of copying it three times. LinkedIn gets the lesson up front. X gets the step-by-step build. Instagram gets the visual argument in eight swipes.&lt;/p&gt;

&lt;p&gt;The whole flow is 40 minutes once the article exists, and most of that is the extraction note that all three pieces share. Write the spine and the four beats once, and the reshaping is fast because you are arranging, not creating.&lt;/p&gt;

&lt;p&gt;Schedule the week in one block so it ships regardless of your mood. Then move on to the next article. This is the repeatable system I run every cluster, and it is one slice of the larger operating manual in the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; if you want to see how the writing, the repurposing, and the scheduling fit into one solo pipeline. Start with your next post. Find the spine before you write a single social caption.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Anthropic Pulled Fable 5 and Mythos 5: What the Ban Means</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:18:54 +0000</pubDate>
      <link>https://dev.to/raxxostudios/anthropic-pulled-fable-5-and-mythos-5-what-the-ban-means-4h7</link>
      <guid>https://dev.to/raxxostudios/anthropic-pulled-fable-5-and-mythos-5-what-the-ban-means-4h7</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Anthropic disabled Fable 5 and Mythos 5 on June 12 after a US government export-control order&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The order bars foreign nationals, so every customer lost access while compliance is sorted out&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The trigger was a narrow jailbreak that asked the model to fix flaws in a codebase&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Opus 4.8 and every other Claude model still work, so that is where I moved my pipelines&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six days after I started routing real work to Claude Fable 5, it vanished. On June 12 Anthropic disabled both Fable 5 and Mythos 5 worldwide, and the reason is not a billing glitch or a quiet deprecation. A US government directive forced the shutdown. Here is what actually happened, why it happened, and the calm fallback I used so none of my projects stalled.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Anthropic launched Fable 5 and Mythos 5 on June 9, the first public models in the new Mythos class. Fable 5 was free on Pro, Max, and Enterprise plans through June 22, it was rolling out across platforms including AWS Bedrock, and the paid pricing was set to kick in on June 23. So a lot of people, me included, spent that week pushing it hard on coding and long-context tasks while it cost nothing. Three days later it was gone.&lt;/p&gt;

&lt;p&gt;In its public statement, Anthropic says it received a government directive on June 12 at 5:21pm Eastern. The company describes it as an export-control directive citing national security authorities. The order bars all access to Fable 5 and Mythos 5 by any foreign national, whether inside or outside the United States, and that explicitly includes Anthropic's own foreign-national employees.&lt;/p&gt;

&lt;p&gt;The practical problem is that you cannot cleanly separate every foreign national from a live consumer product on a few hours of notice. Citizenship is not a field most chat sessions or API keys carry. So the net effect, in Anthropic's own words, is that it had to abruptly disable Fable 5 and Mythos 5 for all customers to stay compliant. Not foreign users only. Everyone. If you had a chat open or an API job queued against either model, it stopped mid-flight, and the model picker simply dropped the options.&lt;/p&gt;

&lt;p&gt;One detail matters for anyone in a panic: every other Claude model is fine. The directive names Fable 5 and Mythos 5 specifically, so Opus 4.8, Sonnet, and Haiku kept running without interruption. I cover the model that absorbed my workload in &lt;a href="https://dev.to/blogs/lab/claude-opus-4-8-is-here-everything-that-changed"&gt;Claude Opus 4.8 Is Here&lt;/a&gt;, and that piece turned into my recovery plan within the hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the government stepped in
&lt;/h2&gt;

&lt;p&gt;The trigger was a jailbreak. Governments worry that a frontier model can be talked into helping with things it should refuse, and the concern here centers on cyber capability: could someone coax Fable 5 into finding and exploiting software vulnerabilities at a level that changes the threat picture for everyone defending real systems.&lt;/p&gt;

&lt;p&gt;Anthropic's account of the demonstrated technique is narrower than the headline suggests. As the company describes it, the jailbreak involved asking the model to read a codebase and fix software flaws, and the flaws it surfaced were minor and already known. Anthropic calls it a narrow, non-universal jailbreak, language that does a lot of work. Narrow means it did not generalize into a master key for arbitrary harmful requests. Non-universal means it did not reliably reproduce across prompts and contexts. In plain terms, the company is arguing this was a corner case, not a kicked-in door.&lt;/p&gt;

&lt;p&gt;That framing lines up with how the Mythos class was sold at launch. The whole pitch was that Mythos-grade capability ships with extra safeguards, including a classifier that can fall back to a safer response on sensitive cyber and bio prompts. I walked through that safety architecture in &lt;a href="https://dev.to/blogs/lab/claude-fable-5-is-here-the-first-public-mythos-class-model"&gt;Claude Fable 5 Is Here&lt;/a&gt;. A model marketed on its safeguards getting pulled over a safeguard gap is the kind of irony that makes a story travel, and it traveled fast.&lt;/p&gt;

&lt;p&gt;I am not going to pretend I can grade the actual risk. I cannot see the red-team report and neither can you. What I can say is that a three-day-old model sitting at the absolute frontier of coding ability is exactly the kind of thing a security agency reacts to fast, and reacting fast tends to mean a blunt instrument rather than a scalpel. A blanket suspension is the bluntest instrument there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it means if you were using it
&lt;/h2&gt;

&lt;p&gt;If you built anything on Fable 5 in that first week, treat it as offline and do not wait for it to blink back on. The honest move is to assume an outage measured in weeks, not hours, and route around it today rather than refreshing a status page.&lt;/p&gt;

&lt;p&gt;For most tasks the swap is painless because the gap between tiers is smaller than the launch benchmarks implied in daily use. Fable 5 was the sharper instrument on the hardest coding problems, and it topped the toughest coding benchmarks at launch, but Opus 4.8 already handled the bulk of my real work and it never went anywhere. I moved my agent loops, my blog generation, and my code review passes straight back to it by swapping a single model name in my config. Nothing else changed. If you are weighing the two tiers for the long run, I put them head to head in &lt;a href="https://dev.to/blogs/lab/claude-fable-5-vs-opus-4-8-is-double-the-price-worth-it"&gt;Claude Fable 5 vs Opus 4.8&lt;/a&gt;, and the short version is that the cheaper tier wins more often than the spec sheet suggests, since Fable 5 listed at roughly twice the price of Opus 4.8 per token once the free window closed.&lt;/p&gt;

&lt;p&gt;There is a quieter lesson here about dependency. I had been tempted to hard-wire a few automations to Fable 5 because it was free through June 22 and genuinely strong on agentic coding. Good thing I did not. When a model can disappear on a government's timetable rather than a vendor's roadmap, you want every pipeline to fall back to a second model with one config change instead of a rewrite. Mine did, so my Monday looked completely normal while the news cycle did not. That is the entire payoff of building model choice into your tooling rather than betting a workflow on one specific name that happened to be hot that week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands now and what I am doing
&lt;/h2&gt;

&lt;p&gt;As of today the models are still down. Anthropic has not restored access, and it has framed the whole episode as a likely misunderstanding. The company says it is working to bring access back as soon as possible and has reportedly sent senior technical staff to Washington for meetings with White House officials to argue that the cited jailbreak does not justify a blanket ban. No firm restoration date has been given, and the reporting I trust suggests it could take weeks rather than days, with enterprise customers being told to plan around the absence rather than wait it out.&lt;/p&gt;

&lt;p&gt;My plan does not depend on any of that resolving. I am keeping production work on Opus 4.8, which is unaffected and already proven in my stack, and I am treating Fable 5 as a nice-to-have that has to re-earn trust before it touches anything that matters. I am not rebuilding around it until it is back and stable for at least a couple of weeks, because a model that came and went once can do it again on the same kind of notice. And I am watching the policy side more closely than the model side, because the precedent is the real story. A capable model was switched off by directive, not by deprecation, and that is a new variable every builder now has to price into how they choose a default model.&lt;/p&gt;

&lt;p&gt;If you only take one action from this, make it boring and useful: confirm that every automation you run can name a fallback model and switch to it without a human in the loop. I keep that wiring documented as part of my &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;, and this week is the clearest argument I have ever had for why it belongs there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Fable 5 was the most capable coding model I had ever pointed at my own repos, and it lasted six days before a government order took it offline for everyone. The cause was a narrow jailbreak involving code-flaw fixing, the scope was a foreign-national export restriction that Anthropic could only honor by disabling the models for all customers, and the current status is still dark with no promised return date. None of that touched Opus 4.8, which is why my work never stopped for a minute. The takeaway is not which model is best this month. It is that any single model can vanish on someone else's schedule, so the resilient setup is the one that shrugs and switches. If you want the version of that setup I actually run, start with the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; and wire a fallback before you need one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Loading Skeletons That Don't Lie: 5 Patterns for Honest Perceived Performance</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Mon, 15 Jun 2026 13:18:18 +0000</pubDate>
      <link>https://dev.to/raxxostudios/loading-skeletons-that-dont-lie-5-patterns-for-honest-perceived-performance-283p</link>
      <guid>https://dev.to/raxxostudios/loading-skeletons-that-dont-lie-5-patterns-for-honest-perceived-performance-283p</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Content-shaped skeletons cut layout shift to zero versus 0.18 CLS for spinners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Match placeholder dimensions to final DOM exactly or you are lying to users&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Spinners beat skeletons under 300ms and for unknown-shape content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Streaming SSR makes the skeleton honest about what is actually arriving&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I rebuilt the loading states on my storefront and watched cumulative layout shift drop from 0.18 to 0.00 on the product grid. The fix was not faster code. It was honest placeholders that matched the final layout pixel for pixel. Here is what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Skeleton Lies When It Does Not Match the Final Layout
&lt;/h2&gt;

&lt;p&gt;Most skeleton screens are decorative. They show three grey bars where the real content has two lines and an image, then the page snaps into a totally different shape when data lands. That snap is a layout shift, and it is the single most annoying thing a loading state can do. The user reads the skeleton, builds a mental model, and then the model is wrong.&lt;/p&gt;

&lt;p&gt;The rule I follow now: a skeleton placeholder must occupy the exact same box as the content it replaces. Same height, same width, same number of lines, same image aspect ratio. If the product card is 320 pixels tall with a 1:1 image and two text lines, the skeleton is 320 pixels tall with a 1:1 grey block and two grey lines. No more, no less.&lt;/p&gt;

&lt;p&gt;This means I stopped building generic skeleton components. A reusable `&lt;code&gt;that renders "three bars" is a trap because it never matches anything. Instead I build a skeleton per layout. The product card has a&lt;/code&gt;ProductCardSkeleton&lt;code&gt;. The article header has an&lt;/code&gt;ArticleHeaderSkeleton`. Each one is the real component with the data swapped for grey shapes, sharing the same CSS grid and the same fixed dimensions.&lt;/p&gt;

&lt;p&gt;Here is the measurement that convinced me. Before, my product grid scored 0.18 CLS in Lighthouse, mostly from images loading without reserved space and text reflowing. After matching skeleton boxes to final boxes and reserving image dimensions with &lt;code&gt;aspect-ratio&lt;/code&gt;, CLS hit 0.00 on three test pages in a row. The grid does not move when data arrives. It just fills in.&lt;/p&gt;

&lt;p&gt;The honesty principle goes deeper than dimensions. If you show four skeleton cards but the response returns six items, you taught the user to expect four. Pull the skeleton count from the same source as the real count when you can, like a cached previous page length or a known page size. A skeleton that promises four and delivers six is a small lie, and small lies in UI add up to distrust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Match the Box: aspect-ratio and Reserved Space Beat Spinners
&lt;/h2&gt;

&lt;p&gt;The mechanical part of zero layout shift is reserving space before anything loads. Images are the worst offenders. An `` with no width and height collapses to zero height, then jumps to full height when the file decodes. The browser cannot reserve space it does not know about.&lt;/p&gt;

&lt;p&gt;Two lines fix most of this. Set &lt;code&gt;aspect-ratio&lt;/code&gt; on the image container and &lt;code&gt;width: 100%&lt;/code&gt; on the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nc"&gt;.card-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the box holds its shape from first paint. The skeleton fills that box with a grey block of identical ratio, and when the real image decodes it slots in without moving a pixel. I do the same for text blocks using fixed line counts and &lt;code&gt;min-height&lt;/code&gt; so a two-line title placeholder cannot become a three-line title.&lt;/p&gt;

&lt;p&gt;The shimmer animation is where people go wrong. A diagonal gradient sweeping across the grey is fine, but it must respect motion preferences. I gate every shimmer behind a media query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.skeleton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that, you are pushing a moving gradient at people who explicitly asked their operating system for less motion. That is an accessibility failure, and it is two lines to fix.&lt;/p&gt;

&lt;p&gt;There is also a performance cost to shimmer that nobody talks about. Animating &lt;code&gt;background-position&lt;/code&gt; on a hundred skeleton elements forces repaints. On a low-end Android device my old shimmer dropped the loading state to 22 frames per second. Switching to a single CSS variable driving one keyframe, and animating &lt;code&gt;opacity&lt;/code&gt; on a pseudo-element instead of background position, brought it back to 60. The loading state should not be the slowest part of the page.&lt;/p&gt;

&lt;p&gt;I store the skeleton dimensions in the same place as the layout tokens. The card height, the image ratio, the gap, all live as CSS custom properties that both the skeleton and the real card read. When I change the card height, both update together. They cannot drift apart because they share one source. If you want the deeper context on reserving storefront layout early, see &lt;a href="https://dev.to/blogs/lab/shopify-section-rendering-api-6-patterns-that-cut-storefront-ttfb-by-60"&gt;Shopify Section Rendering API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Spinner Actually Beats a Skeleton
&lt;/h2&gt;

&lt;p&gt;Skeletons are not universally better. I see teams force skeletons onto everything because they read one article calling spinners outdated. That is wrong. A spinner wins in three concrete situations.&lt;/p&gt;

&lt;p&gt;First, sub-300ms loads. If your data comes back in 180ms, a skeleton flashes for a tenth of a second and the flash itself reads as a glitch. Below roughly 300ms, showing nothing is better than showing a skeleton that vanishes before the eye registers it. I delay loading states by 200ms so fast responses never show a placeholder at all. The content just appears, which is the best possible outcome.&lt;/p&gt;

&lt;p&gt;Second, unknown-shape content. A skeleton only works when you know the layout in advance. If the response could be a list, a single card, an error, or an empty state, you cannot draw a matching placeholder. Drawing a guess means you will almost certainly draw a lie. A centered spinner makes no claim about shape, so it cannot mislead. For search results where I do not know if zero or fifty items come back, I use a spinner, then render the real layout once I know the count.&lt;/p&gt;

&lt;p&gt;Third, full-page transitions and actions like "saving" or "processing payment". Nobody expects a skeleton of a button they just clicked. They expect feedback that the system heard them. A small inline spinner on the button is the honest signal. A skeleton there would be absurd.&lt;/p&gt;

&lt;p&gt;The decision tree I use is simple. Do I know the exact final layout? If no, spinner. Is the load likely under 300ms? If yes, delay then show nothing or spinner. Is it a known content shape over 300ms? Skeleton. Is it a discrete action with no layout? Inline spinner.&lt;/p&gt;

&lt;p&gt;The mistake is treating the choice as fashion. It is an information question. A loading state communicates "what is coming and roughly how big". A skeleton answers that precisely. A spinner answers "something is coming, shape unknown". Pick the one that tells the truth about what you actually know. When you genuinely do not know the shape, pretending you do is the lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming SSR Makes the Skeleton Honest About Timing
&lt;/h2&gt;

&lt;p&gt;The most honest skeleton is one that disappears section by section as real data streams in, not one big swap at the end. Streaming server-side rendering makes this possible. The server sends the page shell immediately, holds open the connection, and flushes each slow section as its data resolves.&lt;/p&gt;

&lt;p&gt;In practice the fast parts of my page (header, navigation, static product details) render instantly with real content. The slow part (personalized recommendations that hit a separate service) shows a skeleton until its data arrives, then streams in and replaces only that region. The skeleton is honest because it covers exactly the part that is genuinely still loading, not the whole page that already finished.&lt;/p&gt;

&lt;p&gt;React Server Components with &lt;code&gt;Suspense&lt;/code&gt; boundaries handle this cleanly. You wrap the slow component in &lt;code&gt;}&amp;gt;&lt;/code&gt; and the framework streams the fallback first, then the real markup. The key detail most people miss: the fallback skeleton must match the resolved component dimensions, or the stream-in causes a layout shift and you have undone all your CLS work. Honesty and zero shift are the same goal.&lt;/p&gt;

&lt;p&gt;I measured the difference on a product page. Without streaming, time to first contentful paint was 1.4 seconds because the whole page waited on the recommendation service. With streaming, FCP dropped to 0.3 seconds because the shell and main product rendered immediately, and only the recommendation strip showed a skeleton for the remaining 1.1 seconds. The user reads the product while the slow part loads behind a placeholder.&lt;/p&gt;

&lt;p&gt;This is where skeletons earn their reputation. A skeleton that covers a section the server has already finished rendering is theater. A skeleton that covers the one section still waiting on a slow upstream call is an accurate status report. Streaming lets you draw the line in exactly the right place.&lt;/p&gt;

&lt;p&gt;One caveat: streaming adds complexity to error handling. If the recommendation service times out, the skeleton must resolve into an error or empty state, never spin forever. I set a hard timeout and a fallback empty state so a stuck stream never leaves a skeleton shimmering into eternity. A skeleton with no exit is the worst lie of all, promising content that will never come.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Honest loading states come down to one idea: never show a placeholder that contradicts what arrives. Match the box exactly so there is zero layout shift. Reserve image space with &lt;code&gt;aspect-ratio&lt;/code&gt;. Gate shimmer behind &lt;code&gt;prefers-reduced-motion&lt;/code&gt;. Use a spinner when you do not know the shape or the load is under 300ms, and a skeleton only when you know the layout in advance. Stream SSR so the skeleton covers the part that is genuinely still loading and nothing else.&lt;/p&gt;

&lt;p&gt;I run all of this on a &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; storefront, and the CLS drop from 0.18 to 0.00 was the most visible single win. The patterns are framework-agnostic though. Any stack with reserved dimensions and streamed sections can do the same.&lt;/p&gt;

&lt;p&gt;If you want the system I use to keep these UI rules consistent across an entire site, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through how I document layout tokens and component contracts so skeletons and real content never drift apart. Start with one component, measure CLS before and after, and let the number prove it.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Headless Claude Code: 5 Things I Run From My GitHub Actions</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:20:05 +0000</pubDate>
      <link>https://dev.to/raxxostudios/headless-claude-code-5-things-i-run-from-my-github-actions-1mm4</link>
      <guid>https://dev.to/raxxostudios/headless-claude-code-5-things-i-run-from-my-github-actions-1mm4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Claude Code -p runs headless in CI with zero terminal&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blog generation, daily audit, PR triage, release notes, README sync all on cron&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each run costs 0.10 to 0.60 EUR depending on context size&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pin the CLI version and cache state or you will burn tokens&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run five jobs from GitHub Actions that used to sit on my laptop. No terminal open, no me watching a progress bar. Claude Code in headless mode does the work on a schedule and opens a pull request when it has something to show me. Here is exactly what runs, what it costs, and the parts that broke before they worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why headless Claude Code beats a local terminal
&lt;/h2&gt;

&lt;p&gt;Most people use Claude Code interactively. You open a terminal, type a request, watch it edit files, approve a diff. That is fine for one-off work. It does not scale to anything that needs to happen every day at 6am while I am asleep.&lt;/p&gt;

&lt;p&gt;The trick is the &lt;code&gt;-p&lt;/code&gt; flag. It runs Claude Code in print mode: you pass a prompt, it does the work, it prints the result, and it exits. No interactive session, no approval prompts. That makes it a normal command line tool, which means GitHub Actions can run it the same way it runs a test suite.&lt;/p&gt;

&lt;p&gt;A minimal job looks like this. You install the CLI, set your API key as a secret, and call it with a prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude -p "Read CHANGES.md and write release notes to RELEASE.md"&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;CLAUDE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLAUDE_KEY }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole pattern. Everything else is variations on it. The job clones my repo, Claude reads files and writes files, and a later step commits the changes or opens a pull request.&lt;/p&gt;

&lt;p&gt;The reason this matters is consistency. When work depends on me remembering to run a script, it happens maybe three times a week. When it runs on a cron trigger, it happens 365 days a year without a single missed day. My daily audit has run 90 times in a row now. I would have skipped at least 40 of those by hand.&lt;/p&gt;

&lt;p&gt;Costs are lower than people expect. A typical headless run uses between 15,000 and 80,000 tokens depending on how much context it reads. That lands between 0.10 and 0.60 EUR per run. My five jobs combined cost under 12 EUR a month. The GitHub Actions minutes are free for the volume I use.&lt;/p&gt;

&lt;p&gt;The one mental shift: headless Claude cannot ask you questions. If it gets confused, it guesses and moves on. So your prompts have to be more precise than they would be in a chat. I write them like specs, not like requests. If you want the full setup behind all of this, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; walks through the foundation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blog generation and the daily audit
&lt;/h2&gt;

&lt;p&gt;Two of my five jobs touch this site directly. The first generates blog drafts. The second audits what is already published.&lt;/p&gt;

&lt;p&gt;The generation job runs three times a week. It pulls a topic from a queue file, reads three existing articles for voice reference, and writes a full draft following a structure spec. The output goes into a pull request, never straight to publish. I read every draft before it goes live, because headless mode will confidently produce a paragraph that is technically wrong if the prompt leaves room.&lt;/p&gt;

&lt;p&gt;The prompt for this job is long, around 600 words. It includes the word count target, the section structure, the banned words, and three example openings. The more I front-load into the prompt, the less I fix afterward. When I started, I gave it two sentences of instruction and spent 20 minutes editing each draft. Now the prompt does the heavy lifting and I spend 4 minutes.&lt;/p&gt;

&lt;p&gt;The daily audit is the job I am proudest of. Every morning at 7am it reads my last 10 published articles and checks them against a rule list: broken internal links, missing affiliate disclosures, headings out of order, word counts that drifted out of range. It writes a report to a markdown file and, if it finds something broken, opens an issue with the specific fix.&lt;/p&gt;

&lt;p&gt;In 90 days it has caught 31 real problems. Eleven were dead internal links where I renamed a handle and forgot to update the references. Nine were articles that drifted under my word floor. The rest were formatting drift. None of those would I have found by hand, because who re-reads their own archive every morning.&lt;/p&gt;

&lt;p&gt;The audit costs more than the other jobs because it reads more files. A full run is around 70,000 tokens, so about 0.50 EUR. Over a month that is 15 EUR for a job that has saved me from publishing broken links 31 times. Easy trade.&lt;/p&gt;

&lt;p&gt;One gotcha: the audit used to read every article in the repo, which pushed token use past 200,000 and the cost past 1.50 EUR per run. I capped it at the 10 most recent files by modified date. Same value, a fifth of the cost. If a job feels expensive, the answer is almost always "you are feeding it too much context."&lt;/p&gt;

&lt;h2&gt;
  
  
  PR triage and release notes
&lt;/h2&gt;

&lt;p&gt;The third and fourth jobs handle the boring parts of maintaining the repo itself.&lt;/p&gt;

&lt;p&gt;PR triage runs whenever a pull request opens. Most of my pull requests come from the blog generation job, but I also get the occasional manual one. The triage job reads the diff, checks it against my rules, and posts a comment summarizing what changed and whether anything looks off. For a blog draft it confirms the word count is in range and the internal links resolve. For a code change it flags anything that touches secrets or the publish flow.&lt;/p&gt;

&lt;p&gt;This is not code review in any deep sense. It is a first-pass filter that catches the obvious stuff so I do not have to. The comment shows up within 90 seconds of the pull request opening. By the time I look at it, the summary is already there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;triage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g @anthropic-ai/claude-code&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claude -p "Review the diff in this PR against the rules in RULES.md. Post a 5-line summary."&lt;/span&gt; 
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CLAUDE_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLAUDE_KEY }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Release notes are the fourth job. Every time I tag a version, this job reads the commit messages since the last tag, groups them by type, and writes human-readable release notes. My commit messages are terse and ugly. The release notes that come out are clean and grouped under headings like "Fixes" and "New." It turns "fix typo in audit prompt lol" into a proper changelog entry without me thinking about it.&lt;/p&gt;

&lt;p&gt;The cost here is tiny, around 0.10 EUR per run, because commit messages are short. This job has written 14 release entries. I have edited exactly two of them. The rest shipped as written.&lt;/p&gt;

&lt;p&gt;The biggest gotcha across both jobs is permissions. GitHub Actions needs explicit write permission to post comments and commit. I lost an hour to a job that ran perfectly but silently failed to post because the workflow had read-only token scope. Add &lt;code&gt;permissions: pull-requests: write&lt;/code&gt; and &lt;code&gt;contents: write&lt;/code&gt; at the job level. If you schedule social posts off the back of a release, &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; takes a webhook and handles the queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  README sync and the gotchas that cost me money
&lt;/h2&gt;

&lt;p&gt;The fifth job keeps my README honest. Documentation drifts. You change a flag, you add a script, you rename a folder, and the README quietly becomes a lie. This job reads the actual project structure and the current scripts, then rewrites the relevant README sections to match reality.&lt;/p&gt;

&lt;p&gt;It runs weekly. It opens a pull request with the diff so I can see exactly what changed before it lands. About one week in three it has a real change. The other weeks it opens nothing, which is the correct behavior. A job that does nothing when there is nothing to do is a good job.&lt;/p&gt;

&lt;p&gt;Now the gotchas, because these cost me real tokens before I fixed them.&lt;/p&gt;

&lt;p&gt;First, pin the CLI version. I ran &lt;code&gt;npm install -g @anthropic-ai/claude-code&lt;/code&gt; with no version, and one morning a new release changed the default model and my token use jumped 40 percent overnight. Pin it: &lt;code&gt;@anthropic-ai/claude-code@2.0.1&lt;/code&gt;. Upgrade on purpose, not by accident.&lt;/p&gt;

&lt;p&gt;Second, cache nothing you do not understand. I tried caching Claude's internal state between runs to save context reads. It saved tokens but produced stale output because the cache held a version of a file that had changed. For these jobs, fresh reads every time are worth the extra 0.05 EUR. Cache the npm install, not the reasoning.&lt;/p&gt;

&lt;p&gt;Third, set a hard timeout. A headless job that gets stuck in a loop will keep calling the API until something stops it. I add &lt;code&gt;timeout-minutes: 10&lt;/code&gt; to every job. One runaway run cost me 3 EUR before the timeout existed. Now the worst case is bounded.&lt;/p&gt;

&lt;p&gt;Fourth, never let a headless job publish without a human gate. Every one of my five jobs either opens a pull request or writes a draft. None of them push to production directly. Headless Claude is good, not perfect, and the cost of a bad article going live is much higher than the cost of me reading a draft for 4 minutes.&lt;/p&gt;

&lt;p&gt;Fifth, log the token count. Claude Code prints usage at the end of a run. Pipe it to a file and check it weekly. That is how I caught the version bump that doubled my costs. Visibility is the whole game when the work happens while you sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Headless Claude Code turned five chores I used to skip into five jobs that run themselves. The blog drafter, the daily audit, the PR triage, the release notes, and the README sync cost me under 12 EUR a month combined and save me hours I was not spending anyway, because half of them I simply never did by hand.&lt;/p&gt;

&lt;p&gt;The pattern is the same every time: install the CLI, pass a precise prompt with &lt;code&gt;-p&lt;/code&gt;, write the output to a file, open a pull request, keep a human gate before anything ships. Pin your version, set a timeout, log your tokens, and feed each job the smallest amount of context that still does the work.&lt;/p&gt;

&lt;p&gt;If you want the full foundation behind how I run Claude as infrastructure rather than a chat window, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; is where I keep the setup. Start with one job. The daily audit is the easiest win and the one most likely to catch something embarrassing before your readers do.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Why I Stopped Using Cursor for Production Code (And What I Use Now)</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Thu, 11 Jun 2026 00:17:18 +0000</pubDate>
      <link>https://dev.to/raxxostudios/why-i-stopped-using-cursor-for-production-code-and-what-i-use-now-58dj</link>
      <guid>https://dev.to/raxxostudios/why-i-stopped-using-cursor-for-production-code-and-what-i-use-now-58dj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cursor felt fast for prototypes but stalled on real production tasks&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Claude Code hooks let me automate checks before every commit&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skills and plugins turned a chat tool into a real pipeline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I shipped 14 product launches in one quarter with the new setup&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ran my entire studio on Cursor for about eight months. Then I switched to Claude Code and shipped 14 product launches in a single quarter, more than I managed in the previous three combined. This is the honest version of why I left and what actually changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cursor Did Well And Where It Broke
&lt;/h2&gt;

&lt;p&gt;Cursor is a genuinely good editor. I want to say that up front because the internet loves a clean villain and Cursor is not one. For prototyping it is excellent. I would open a fresh file, describe a small feature, and watch it scaffold something usable in under a minute. The tab completion is sharp. The inline edit feature, where you highlight a block and ask for a change, saved me real time on small refactors.&lt;/p&gt;

&lt;p&gt;The problem started when my projects grew past the toy stage. My main &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; backend has about 40 files that all touch each other. Product sync, image pipelines, scheduling logic, the blog publisher. Once a codebase reaches that size, the editor needs to understand relationships, not just the file in front of it. Cursor would confidently edit one file and quietly break two others. It did not know my conventions. It did not run my tests. It treated every request as a fresh conversation with no memory of the rules I had set up the day before.&lt;/p&gt;

&lt;p&gt;I started keeping a text file open with my own rules, pasting it into the chat every session. Naming conventions, the fact that I never use em dashes in output, the currency formatting I require, the list of words my publish script blocks. Every single session I pasted this. After two months of pasting the same 600 words I realized I had built a manual workflow around a tool that was supposed to remove manual work.&lt;/p&gt;

&lt;p&gt;The breaking point was a Friday in March. I asked Cursor to add a field to my product schema. It updated the schema, missed three call sites, and I pushed broken code that took a live page down for 20 minutes. Not catastrophic. But it was the third time that month. The tool was fast at writing and slow at being correct, and for production work correct is the only speed that counts. I stopped trusting it for anything I planned to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Claude Code Hooks Changed My Daily Work
&lt;/h2&gt;

&lt;p&gt;The single feature that moved me was hooks. Claude Code lets you define commands that fire automatically at specific moments. Before a tool runs, after it edits a file, before a commit. This sounds small. It is the whole game.&lt;/p&gt;

&lt;p&gt;I set up a hook that runs my linter and word checks every time a file gets written. Now when the model edits my blog publisher, the hook immediately runs my forbidden-words scan and flags any em dash or blocked term before I ever see the output. The error comes back into the conversation and Claude fixes it on the spot. No manual paste. No friday afternoon outage. The rules live in the project, not in my memory.&lt;/p&gt;

&lt;p&gt;I have a second hook that runs my test suite after any change to the schema files. If a field gets added and three call sites break, the tests fail, the failures land back in the session, and the fix happens in the same loop. The thing that took my site down in March is now caught in about four seconds, automatically, every time.&lt;/p&gt;

&lt;p&gt;This is the difference between an editor and a system. Cursor wrote code into my files. Claude Code participates in my actual process: write, check, fix, verify, then move on. I went from pasting 600 words of rules per session to defining them once in a config and forgetting about them.&lt;/p&gt;

&lt;p&gt;If you want the deeper context on the full setup, &lt;a href="https://dev.to/blogs/lab/claude-code-desktop-full-ide-rebuild"&gt;Claude Code Desktop Full IDE Rebuild&lt;/a&gt; goes through how I wired the whole thing together. The short version: my rules became infrastructure. A new project inherits the same hooks the moment I clone my template. The forbidden words, the currency format, the test gates, all of it loads automatically.&lt;/p&gt;

&lt;p&gt;The honest tradeoff is setup cost. Hooks take an afternoon to configure properly the first time. Cursor needs zero setup. But I run my studio for years, not for one afternoon, and the math favors the tool that pays me back every single day after that first afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills And Plugins Turned It Into A Pipeline
&lt;/h2&gt;

&lt;p&gt;Hooks fixed correctness. Skills and plugins fixed scope. A skill in Claude Code is a packaged capability you teach once and reuse forever. I built a skill for my blog format, the exact structure you are reading now. TLDR div on line one, four H2 sections, a bottom line, internal links, affiliate placement. I no longer explain the format. I say "write a lab article on X" and the skill carries every rule.&lt;/p&gt;

&lt;p&gt;I built another skill for my product launch checklist. When I drop a new design into the system, the skill knows the sequence: generate the mockups, upscale the hero image, write the listing, schedule the social posts, update the index. Each step that used to be a separate tool sits inside one reusable capability now.&lt;/p&gt;

&lt;p&gt;The image step runs through &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; for upscaling because my source art comes out at low resolution and needs to be print-ready. The social scheduling step hands off to &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; so a launch fans out across platforms without me opening five tabs. Claude Code orchestrates the whole chain. Cursor never tried to be this. It is an editor and it stays in its lane, which is fine, but my work is not editing. My work is shipping finished products end to end.&lt;/p&gt;

&lt;p&gt;Plugins extend this further. I run my &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; store operations through a plugin that lets the model read and write product data directly. That means a launch goes from idea to live listing inside one session, with the test gates and word checks firing the whole way. The 14 launches I mentioned came out of this exact pipeline. Each one would have been a half-day of manual stitching in my old setup. Now most take under an hour of my actual attention.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning: the tool that wins is not the one with the slickest autocomplete. It is the one that lets me encode my process so I stop repeating myself. Cursor optimized for the keystroke. Claude Code optimized for the workflow, and the workflow is where solo studios live or die.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Would Still Reach For Cursor
&lt;/h2&gt;

&lt;p&gt;I am not going to pretend Claude Code wins everything, because that is the kind of post that ages badly and helps nobody. There are cases where I would still open Cursor today.&lt;/p&gt;

&lt;p&gt;If I am learning a new framework and I want to poke at it interactively, Cursor's inline editing and instant completion are lovely. The feedback loop is tight. I can type half a thought and see it finished. For pure exploration where correctness does not matter yet, the fast loop beats the structured one. I keep Cursor installed for exactly this.&lt;/p&gt;

&lt;p&gt;If you work on a large team with an established editor culture, the calculus also shifts. My whole argument rests on being solo. I define the rules, I own the hooks, I never have to negotiate conventions with five other developers. A team has different friction, and an editor everyone already knows has real value that a single-operator workflow does not capture. I am describing what works for one person running an entire studio, not issuing a universal verdict.&lt;/p&gt;

&lt;p&gt;And if your projects genuinely stay small, Cursor's zero setup is a feature, not a gap. The hook system I love is overhead you do not need if your codebase is three files that never break each other. I would have told my eight-months-ago self to stay on Cursor a while longer, honestly. The switch only pays off once your work outgrows the single-file mindset.&lt;/p&gt;

&lt;p&gt;What pushed me over was the combination: production stakes, solo ownership, and a process complex enough to be worth encoding. When all three are true, hooks and skills stop being nice extras and become the reason the studio runs at all. Background: &lt;a href="https://dev.to/blogs/lab/claude-code-desktop-full-ide-rebuild"&gt;Claude Code Desktop Full IDE Rebuild&lt;/a&gt; walks through the moment those three lined up for me and the setup decisions I made.&lt;/p&gt;

&lt;p&gt;So I keep both tools. I just use them for what each is actually good at, and the heavy production work all flows through the structured one now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;I left Cursor because my work changed, not because the tool got worse. Prototyping rewards speed and a tight feedback loop, and Cursor is excellent there. Production rewards correctness, repeatable process, and a system that remembers my rules so I do not have to. Hooks caught the bugs that took my site down. Skills packaged my formats so I stopped explaining them. Plugins connected the model to my actual store, and 14 launches in a quarter came out the other side.&lt;/p&gt;

&lt;p&gt;If you run a solo operation and you keep pasting the same rules into a chat box every morning, that is the signal. Encode it once. Let the tool check itself. The afternoon you spend on setup pays back every day after.&lt;/p&gt;

&lt;p&gt;If you want the full picture of how I wired my studio around this, the &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt; lays out the hooks, skills, and project structure I use end to end. Start small, automate the check that keeps biting you, and build from there.&lt;/p&gt;

&lt;p&gt;This article contains affiliate links. If you sign up through them, I may earn a small commission at no extra cost to you. (Ad)&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Fable 5 Is Here: The First Public Mythos-Class Model</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Wed, 10 Jun 2026 07:06:42 +0000</pubDate>
      <link>https://dev.to/raxxostudios/claude-fable-5-is-here-the-first-public-mythos-class-model-1bjm</link>
      <guid>https://dev.to/raxxostudios/claude-fable-5-is-here-the-first-public-mythos-class-model-1bjm</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Anthropic released Claude Fable 5 on June 9, 2026, the first Mythos-class model anyone can use&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pricing doubles Opus 4.8: 10 dollars per million input tokens, 50 out, free on paid plans until June 22&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cyber, bio, and distillation prompts get answered by Opus 4.8 instead, under 5 percent of sessions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SWE-Bench Pro hits 80.3 percent vs 69.2 for Opus 4.8, and Stripe ran a 50-million-line migration in a day&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anthropic put a model above Opus on the shelf. On June 9, 2026 it released Claude Fable 5, the first Mythos-class model available to everyone with an API key, alongside Claude Mythos 5 for a restricted partner circle. I switched my whole studio pipeline to Fable 5 on launch morning. This is what shipped, what it costs, and where the catch is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Anthropic Shipped on June 9
&lt;/h2&gt;

&lt;p&gt;Two models, one underlying brain. Claude Fable 5 is the public release. Claude Mythos 5 is the same model with fewer safeguards, reserved for vetted partners. Mythos is not a codename, it is a new model class that sits above Opus in Anthropic's lineup, the way Opus sits above Sonnet. Fable 5 is that class made safe enough to hand to everyone.&lt;/p&gt;

&lt;p&gt;The model ID is &lt;code&gt;claude-fable-5&lt;/code&gt;. It carries a 1 million token context window and up to 128K output tokens, same envelope as Opus 4.8. Pricing is 10 dollars per million input tokens and 50 per million output. That is exactly double Opus 4.8, which stays at 5 and 25.&lt;/p&gt;

&lt;p&gt;Availability moved fast. The API had it on day one. GitHub Copilot flipped it to generally available the same day, Amazon Bedrock listed it, and Claude Code picked it up immediately (this article was produced on it). For subscribers the deal is unusually good: Pro, Max, Team, and seat-based Enterprise plans include Fable 5 at no extra cost from June 9 to June 22. From June 23 it needs usage credits, with a return to standard plans promised once capacity allows. If you pay for Claude, you have a two-week window to test the strongest model Anthropic has ever shipped without spending anything extra.&lt;/p&gt;

&lt;p&gt;For developers the migration is one line, with one trap. Fable 5 keeps the Opus 4.8 request surface: adaptive thinking, no sampling parameters, effort levels from low to max. The trap is that an explicit thinking disabled setting now returns a 400 error. Opus 4.8 tolerates it, Fable 5 does not, so omit the field instead and audit any shared request builder before you swap the ID.&lt;/p&gt;

&lt;p&gt;Back in May I sorted through the leaks and guesses in &lt;a href="https://dev.to/blogs/lab/claudes-next-model-sonnet-4-8-and-mythos-rumors-sorted"&gt;Claude's Next Model: Sonnet 4.8 and Mythos Rumors, Sorted&lt;/a&gt;. The short version: the Mythos rumors were real, and the public version arrived faster than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Locked Lab to Public Release
&lt;/h2&gt;

&lt;p&gt;Fable 5 did not appear from nowhere. In April, Anthropic started Project Glasswing and handed the first Mythos-class model, Claude Mythos Preview, to a small group of cyber defenders and critical infrastructure providers. I covered the program in &lt;a href="https://dev.to/blogs/lab/project-glasswing-anthropics-claude-mythos-cybersecurity-bet"&gt;Project Glasswing: Anthropic's Claude Mythos Cybersecurity Bet&lt;/a&gt; and the unusual logic behind it in &lt;a href="https://dev.to/blogs/lab/anthropic-just-gave-12-companies-their-most-dangerous-ai-model-on-purpose"&gt;Anthropic Just Gave 12 Companies Their Most Dangerous AI Model. On Purpose.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That program just scaled. Glasswing now spans roughly 150 organizations across more than 15 countries, and those partners get Mythos 5 with the cyber safeguards lifted. Selected biology researchers are next in line, with the bio and chemistry restrictions removed for vetted labs. A broader trusted-access program is planned after that.&lt;/p&gt;

&lt;p&gt;The pricing tells its own story: Fable 5 and Mythos 5 cost less than half of what Mythos Preview did. Anthropic is moving this class from experiment to product.&lt;/p&gt;

&lt;p&gt;The timing raised eyebrows. The release landed days after Anthropic published warnings about AI capabilities becoming dangerous, and the press read it as a contradiction. I read it differently. The two-tier release is the answer to their own warning: ship the capability, gate the dangerous slices, and put the ungated version behind vetting. Whether that holds up is a separate question, but it is a coherent position, not an accident.&lt;/p&gt;

&lt;p&gt;One more piece that matters if you run client work through the API: all Mythos-class traffic has a 30-day retention window, is not used for training, and every human access to that data gets logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Opus Handoff: How the Safeguards Work
&lt;/h2&gt;

&lt;p&gt;This is the genuinely new mechanism, and it is the part most coverage gets wrong. Fable 5 does not simply refuse risky requests. Separate classifier models watch every conversation, and when one detects a request in three specific areas, the response is generated by Claude Opus 4.8 instead of Fable 5.&lt;/p&gt;

&lt;p&gt;The three trigger areas: cybersecurity (vulnerability discovery, exploitation, agentic hacking, attack planning), biology and chemistry around dual-use research, and distillation attempts, meaning prompts designed to extract the model's capabilities to train a competitor.&lt;/p&gt;

&lt;p&gt;Anthropic says the classifiers trigger in under 5 percent of sessions on average. They are tuned conservatively, which means some harmless requests get the downgrade too, and the stated goal is to cut those false positives now that it is live.&lt;/p&gt;

&lt;p&gt;The jailbreak numbers are worth quoting. Over 1,000 hours of internal and external bug bounty testing produced no universal jailbreak. Across 30 different public jailbreak techniques on cyberattack tasks, Fable 5 complied with zero harmful single-turn requests. External red teams found the same wall, with the UK AISI making only initial progress on long-form agentic tasks. One external partner called these the toughest cyber safeguards of any model they had tested.&lt;/p&gt;

&lt;p&gt;The gap being protected is real. On ExploitBench, the unrestricted Mythos 5 scores 78 percent against 40 percent for Opus 4.8. That is the capability Anthropic decided not to hand out with an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Extra Tier Buys
&lt;/h2&gt;

&lt;p&gt;The benchmark sweep is broad, so I will stick to the numbers that changed my routing. SWE-Bench Pro, which tests real GitHub engineering tasks, lands at 80.3 percent for Fable 5 against 69.2 for Opus 4.8, 58.6 for GPT-5.5, and 54.2 for Gemini 3.1 Pro. On Cognition's FrontierCode, the hard production-standard set, Fable 5 scores 29.3 against Opus 4.8's 13.4. More than double on the tasks that actually hurt.&lt;/p&gt;

&lt;p&gt;Outside coding: first model past 90 percent on the Core Analytics benchmark, a 10-point jump over Opus. Top score on the Hebbia finance benchmark for document and chart reasoning. On an everyday spreadsheet suite it beats Opus 4.8 at every effort level while finishing 25 to 30 percent faster.&lt;/p&gt;

&lt;p&gt;The customer stories land harder than the benchmarks. Stripe compressed a Ruby migration across a 50-million-line codebase, previously scoped at months of engineering, into a single day. Cursor's CEO called it state of the art on their internal benchmark and said it opened long-horizon problems that were previously out of reach.&lt;/p&gt;

&lt;p&gt;Two capabilities stand out as actually new. Vision: Fable 5 rebuilt working web app source code from screenshots, pulled precise numbers off scientific figures, and beat Pokemon FireRed using a vision-only harness, something earlier models needed helper tools for. And memory: on long Slay the Spire runs, persistent memory improved its performance 3 times more than it did for Opus 4.8, and it reached the final act 3 times more often.&lt;/p&gt;

&lt;p&gt;The science results read like a different product category. Internal experts report drug design work running about 10 times faster, with 9 of 14 protein targets yielding strong candidates. In blinded comparisons, its molecular biology hypotheses were preferred about 80 percent of the time over Opus-class output, and one of those hypotheses was later corroborated by an independent published study. In genomics it ran a week of autonomous research and trained a custom model 100 times smaller than a Science-published baseline while outperforming it. Anthropic describes the level as senior research scientist, picking directions and allocating resources, and for the first time that claim does not read as pure marketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Fable 5 is the new ceiling, and for once the ceiling is not locked behind a waitlist. The two-week free window on paid plans (June 9 to 22) is the right time to find out what it does to your own workload, because after June 23 every run costs credits.&lt;/p&gt;

&lt;p&gt;My early read after a day of real use: the jump is visible on long autonomous tasks and barely visible on routine ones, which is exactly what the benchmark spread predicts. Opus 4.8 does not retire, it becomes the value pick, and I will publish the full head-to-head routing math separately. For the baseline of what Opus 4.8 brought three weeks ago, start with &lt;a href="https://dev.to/blogs/lab/claude-opus-4-8-is-here-everything-that-changed"&gt;Claude Opus 4.8 Is Here: Everything That Changed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want the Claude Code setup I ran this launch-day test on, hooks, commands, and guardrails included, it ships as &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude Blueprint&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
