<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: RAXXO Studios</title>
    <description>The latest articles on Forem by RAXXO Studios (@raxxostudios).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3848289%2Ffd2912c9-5820-4993-8fdc-62ec1e778980.png</url>
      <title>Forem: RAXXO Studios</title>
      <link>https://forem.com/raxxostudios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/raxxostudios"/>
    <language>en</language>
    <item>
      <title>Claude's Next Model: Sonnet 4.8 and Mythos Rumors, Sorted</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sun, 24 May 2026 13:26:49 +0000</pubDate>
      <link>https://forem.com/raxxostudios/claudes-next-model-sonnet-48-and-mythos-rumors-sorted-1pne</link>
      <guid>https://forem.com/raxxostudios/claudes-next-model-sonnet-48-and-mythos-rumors-sorted-1pne</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Claude Opus 4.7 shipped 2026-04-16 and is the current public flagship, not a rumor&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Claude Sonnet 4.8 is not announced, traced to one leaked string in Claude Code source&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Claude Mythos exists and is real, but it is restricted to a handpicked group of companies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Anthropic conceded on 2026-04-16 that Opus 4.7 still trails its Mythos Preview model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Plan on Opus 4.7 and Sonnet 4.6 today, do not pause work for an undated model&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The internet decided Claude has a secret super-model and a phantom Sonnet 4.8 on the way. Some of that is true. Most of it is not. I sorted the confirmed releases from the leak-blog fan fiction so you can plan a build without chasing ghosts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually shipping right now
&lt;/h2&gt;

&lt;p&gt;Start with what I can point at and use today. No leaks required.&lt;/p&gt;

&lt;p&gt;Claude Opus 4.7 went generally available on 2026-04-16. It is the current public flagship. Anthropic's own announcement frames it as a coding and long-running-task model, with a "notable improvement on Opus 4.6 in advanced software engineering" and the ability to handle "complex, long-running tasks with rigor and consistency."&lt;/p&gt;

&lt;p&gt;The numbers Anthropic published are specific, so I trust them more than any rumor table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CursorBench: 70 percent, versus Opus 4.6 at 58 percent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BigLaw Bench: 90.9 percent accuracy at high effort.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rakuten-SWE-Bench: resolves roughly 3x more production tasks than Opus 4.6.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vision: accepts images up to 2,576 pixels on the long edge (about 3.75 megapixels), more than 3x the pixels of earlier Claude models.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pricing for Opus 4.7 stayed put at 5 EUR-equivalent per million input tokens and 25 per million output tokens (Anthropic lists it in dollars, but the ratio is what matters for planning). That is the same as Opus 4.6, which is the rare case where a stronger model did not get more expensive. For a solo operator that detail matters: a capability jump with a flat token cost means I can upgrade my hardest workloads without re-running the math on every batch job.&lt;/p&gt;

&lt;p&gt;On the Sonnet side, Claude Sonnet 4.6 released 2026-02-17 and is still the current Sonnet. It is the workhorse I reach for on volume tasks: cheaper, fast, good enough for most of my drafting and parsing jobs while Opus 4.7 handles the hard reasoning. I split work between the two on purpose. Routing every call to the flagship is how a small studio burns a token budget for no real gain. Sonnet 4.6 does the bulk grind, Opus 4.7 does the thinking, and that two-tier split has held steady through this whole rumor cycle.&lt;/p&gt;

&lt;p&gt;So the lineup as of late May 2026 is simple. Opus 4.7 for the heavy work. Sonnet 4.6 for the rest. Everything past this point is either a countdown or a rumor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The June 15 deadline that is not a rumor
&lt;/h2&gt;

&lt;p&gt;Before chasing future models, clear the one date that will actually break things if you ignore it. The original Claude Opus 4 and Claude Sonnet 4 retire on 2026-06-15. After that, requests pinned to those model IDs stop working.&lt;/p&gt;

&lt;p&gt;This is the boring, confirmed news that matters more than any leak. I have covered the specifics in &lt;a href="https://dev.to/blogs/lab/claude-opus-4-and-sonnet-4-retire-june-15"&gt;the June 15 retirement of Opus 4 and Sonnet 4&lt;/a&gt;, so I will keep it short here.&lt;/p&gt;

&lt;p&gt;The migration is mechanical. If you call &lt;code&gt;claude-opus-4&lt;/code&gt; or &lt;code&gt;claude-sonnet-4&lt;/code&gt; anywhere (a script, an agent, a hardcoded config, a cron job you forgot about), swap the model ID to a current one before the cutoff.&lt;/p&gt;

&lt;p&gt;My quick checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Grep your whole codebase for old model strings, not just the obvious app code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move heavy reasoning calls to Opus 4.7. Move volume calls to Sonnet 4.6.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-run your eval set after swapping. Prompts tuned for an older model sometimes drift on a newer one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check any third-party tools or plugins you depend on. They have their own deadlines.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason I lead with this instead of the shiny rumors: a hard retirement date is a fact you can act on this week. A leaked string about Sonnet 4.8 is not. One of these will cause a 2 AM outage if you ignore it. The other will not exist for months, if at all.&lt;/p&gt;

&lt;p&gt;If your stack still references the retiring models, fix that first. Then come back and enjoy the speculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Sonnet 4.8: where the rumor came from
&lt;/h2&gt;

&lt;p&gt;Here is the headline a lot of blogs ran: Claude Sonnet 4.8 is coming. Here is the honest version: it is not announced, and the evidence is thin.&lt;/p&gt;

&lt;p&gt;As of 2026-05-23, Anthropic has made no announcement, published no API model ID, released no benchmarks, and given no date for a Sonnet 4.8. So where did the rumor start?&lt;/p&gt;

&lt;p&gt;A single leaked string. In late March 2026, a JavaScript source map was accidentally shipped with Claude Code, and someone spotted a "4.8" reference inside it. That string is the entire signal. Nothing came with it. No spec, no date, no confirmation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I am skeptical, not dismissive
&lt;/h3&gt;

&lt;p&gt;A string in a source map is not nothing. Internal builds reference unreleased things all the time. But it is also not a release. A few reasons I am keeping this in the rumor column:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The originating report flagged itself as uncertain. The post that surfaced it added a question mark, which tells you the finder was not confident either.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A prediction market on a Sonnet 4.8 release by 2026-05-24 sat at around 3 percent. The crowd does not believe it is imminent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It would break Anthropic's versioning pattern. Opus 4.5 and Sonnet 4.5 matched. Opus 4.6 and Sonnet 4.6 matched. Jumping Sonnet to 4.8 with no 4.7 in between would skip a step Anthropic has never skipped.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the one I weigh most. Anthropic has been consistent: the Opus and Sonnet minor versions move together. A lone "4.8" that leaps past a Sonnet 4.7 is more likely a placeholder, a test ID, or an internal experiment than a near-term product. Labs leave dead strings and forward-looking IDs in their code constantly. A grep hit is a clue, not a launch date.&lt;/p&gt;

&lt;p&gt;There is also a tell in how these posts get written. The headline says "Claude Sonnet 4.8 Release Date" with full confidence. The body, if you read it, walks it all back and admits nothing is announced. That gap between headline and body is the cleanest signal that a piece is chasing search traffic rather than reporting news.&lt;/p&gt;

&lt;p&gt;My rule for any reader who finds this article while deciding what to build on: if a post hands you a Sonnet 4.8 benchmark table, a price, or a confirmed release date right now, it is fabricating. Treat it that way. The honest answer in late May 2026 is "no Sonnet 4.8 exists yet," and that answer will not change until Anthropic publishes a model page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Mythos and the Opus 5 question
&lt;/h2&gt;

&lt;p&gt;The Mythos story is the opposite of the Sonnet 4.8 story. Mythos is real. Anthropic confirmed it. The fan fiction is in the details, not the existence.&lt;/p&gt;

&lt;p&gt;On 2026-04-16, the same day Opus 4.7 shipped, Anthropic publicly conceded that Opus 4.7 does not match the performance of a more capable internal model called Mythos. Per Axios, the company said benchmark charts show Opus 4.7 beating ChatGPT 5.4 and Gemini 3.1 Pro, while still falling short of "its Mythos Preview model." That is an unusual move: a lab admitting in public that it is sitting on something stronger than what it just released.&lt;/p&gt;

&lt;p&gt;Mythos is not public. Axios reports access is limited to "a handpicked group of tech and cybersecurity companies," and the model is withheld over safety concerns. Anthropic's stated goal is "a broad release of Mythos-class models" once it learns enough from deploying safeguards.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is confirmed vs what is leak-blog guesswork
&lt;/h3&gt;

&lt;p&gt;Mythos first surfaced through a large source-code and document exposure. A misconfigured data store left around 3,000 internal files reachable without authentication in late March 2026, then a researcher disclosed it and Anthropic locked it down within hours. I covered that incident in &lt;a href="https://dev.to/blogs/lab/512-000-lines-of-leaked-code-exposed-anthropics-secret-models"&gt;the source leak that first surfaced Mythos&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;From the leak and Anthropic's own statements, here is my confidence split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Confirmed: Mythos exists, and Anthropic calls it a major capability advance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Confirmed: It is restricted, withheld for safety, given to a small set of companies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reported but unverified: Mythos is "far ahead of any other AI model in cyber capabilities," and can find and exploit vulnerabilities faster than human defenders. That phrasing comes from leaked docs, not a published benchmark.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Not confirmed: the name "Opus 5." The community attached that label. Leaked docs frame Mythos as a new capability class, not a version bump, and Anthropic has not blessed "Opus 5."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cybersecurity angle is the most consequential part of the story and the part with the least public proof. If you want the security framing, I dug into &lt;a href="https://dev.to/blogs/lab/project-glasswing-anthropics-claude-mythos-cybersecurity-bet"&gt;Mythos powering Project Glasswing&lt;/a&gt;. Read it knowing the specifics are still reported, not benchmarked.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I plan around all of this as a solo builder
&lt;/h2&gt;

&lt;p&gt;Strip the drama and the planning answer is short. Build on what exists. Watch the rumors for free.&lt;/p&gt;

&lt;p&gt;I run a one-person studio, so I cannot afford to architect around a model that might land in Q3, might be named something else, and might never go public in my tier. Here is the framework I actually use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat the timeline as three buckets
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Shipping now: Opus 4.7, Sonnet 4.6. These are the only things I let into production.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Confirmed but inaccessible: Mythos. Real, stronger, not available to me. I track it, I do not depend on it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rumored: Sonnet 4.8, "Opus 5," "Claude 5," and the speculated Q2 to Q3 2026 window for the next major model. None of these names are official. None get a line item in my plans.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My practical rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pin to current model IDs, but isolate them. I keep the model name in one place so a future swap is a one-line change, not a hunt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never delay a launch for an unannounced model. The cost of waiting is certain. The arrival of Sonnet 4.8 is not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-run evals on every model bump. A newer model is not automatically better for your exact prompt. Measure, do not assume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Read the leak blogs for direction, not numbers. They are useful for "Anthropic is clearly investing in cyber and long-horizon agents." They are useless for "here is the exact benchmark."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep a one-line note on the rumor, then move on. I track Mythos and Sonnet 4.8 in a single text file with a date and a confidence tag. That is the right amount of attention for something I cannot use yet.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The direction Anthropic is signaling is not subtle. It conceded in public that it has a stronger model than it ships. It is gating that model on safety and handing it to a narrow set of partners first. It keeps shipping minor versions of Opus and Sonnet on a tight cadence. Put those together and the realistic read is more capable models arriving steadily, with the frontier ones arriving slowly and to a few hands. None of that tells me a name or a date. All of it tells me the safe bet is to build clean on today's models and keep the swap cheap.&lt;/p&gt;

&lt;p&gt;The signal here is that Anthropic has a stronger model than it ships, and a steady cadence of minor versions. The noise is every specific date, score, and name attached to models that do not have a public page yet. If you want the same model setup I run day to day, I keep it documented in &lt;a href="https://dev.to/pages/claude-blueprint"&gt;my Claude setup blueprint&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Here is the whole thing in one breath. Opus 4.7 (shipped 2026-04-16) and Sonnet 4.6 (shipped 2026-02-17) are real and usable today. Opus 4 and Sonnet 4 retire 2026-06-15, so migrate before then. Mythos is real, stronger than Opus 4.7 by Anthropic's own admission, and locked to a small set of companies for safety reasons. Claude Sonnet 4.8 is not announced and rests on a single leaked string. "Opus 5" and "Claude 5" are community labels, not confirmed names, and any next-major-model date in the Q2 to Q3 2026 range is speculation.&lt;/p&gt;

&lt;p&gt;If you only do one thing this week, it is not waiting for a phantom model. It is grepping your codebase for the retiring model IDs and swapping them. That is the deadline with teeth.&lt;/p&gt;

&lt;p&gt;I will keep updating as Anthropic actually announces things, not as leak blogs invent them. If you want a clean starting point that already runs on the current models and is easy to re-point when the next one lands, take a look at &lt;a href="https://dev.to/pages/claude-blueprint"&gt;my Claude setup blueprint&lt;/a&gt; and skip the guesswork.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude's Big May: Limit Reset, Code w/ Claude, Small Business</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sun, 24 May 2026 13:26:13 +0000</pubDate>
      <link>https://forem.com/raxxostudios/claudes-big-may-limit-reset-code-w-claude-small-business-35a4</link>
      <guid>https://forem.com/raxxostudios/claudes-big-may-limit-reset-code-w-claude-small-business-35a4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;On May 15 2026 Anthropic manually reset everyone's 5-hour and weekly Claude limits across Pro, Max, Team, and Enterprise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;At its Code w/ Claude events, roughly half a packed room said Claude wrote a pull request they shipped in the past week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Claude for Small Business launched May 13 2026 with 7 connectors and 15 ready-to-run agentic workflows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;KPMG put Claude in front of 276,000+ employees and PwC expanded to Claude Code and Cowork.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A solo studio gets the same agentic workflows the big firms just bought, minus the seat count.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;May 2026 was the month Claude stopped feeling like a tool I open and started feeling like infrastructure other people build companies on. Four things blew up in four weeks: a surprise limit reset, a developer conference moment that made the internet flinch, a small-business package, and two consulting giants going all-in. Here is what each one actually was, and what it changes for a one-person studio like mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enterprise wave nobody saw coming this fast
&lt;/h2&gt;

&lt;p&gt;Two announcements bookended the month and they matter even if you never touch enterprise software. On 2026-05-19 Anthropic announced a global alliance with KPMG, embedding Claude inside KPMG's "Digital Gateway" platform with access for more than 276,000 employees. Around the same window, PwC expanded its existing alliance, rolling out Claude Code and Cowork starting with US teams.&lt;/p&gt;

&lt;p&gt;I am not a 276,000-person firm. So why do I care? Because when the big consultancies standardize on a model, three things happen downstream. The model gets hardened for boring, high-stakes work (audit trails, compliance, document review), which trickles into the same product I pay 20 EUR a month for. The integrations get built once and reused everywhere. And the "is AI ready for real work" debate quietly ends, which makes my own AI-built products easier to sell to skeptical buyers.&lt;/p&gt;

&lt;p&gt;The Digital Gateway detail is the part I keep thinking about. KPMG did not bolt a chatbot onto its website. It put Claude inside the platform its people already use to do audits and advisory work, in front of more than a quarter of a million employees. That is the difference between a pilot and a commitment. When a firm whose entire product is professional judgment decides Claude is safe enough to sit in the workflow, the bar for "trustworthy AI" just got set in public by people who get sued for being wrong.&lt;/p&gt;

&lt;p&gt;There is also a competitive read here. I have written before about &lt;a href="https://dev.to/blogs/lab/anthropic-now-owns-40-of-enterprise-llm-spend-and-what-that-means-for-solo-builders"&gt;Anthropic's lead in enterprise AI spend&lt;/a&gt;, and these two deals are that thesis playing out in public. KPMG and PwC are not experiments. They are committed budgets and headcount. For a solo builder, that is free validation: the foundation I rely on is being battle-tested by people with far more to lose than me.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I am not going to do
&lt;/h3&gt;

&lt;p&gt;I am not going to deep-dive the enterprise economics here. The short version: the firms get a model that handles document-heavy, repeatable work at scale, and Anthropic gets reference logos plus enormous usage data. If you want the full breakdown of what enterprise dominance means for small builders, I covered it separately. For this roundup, the takeaway is simpler. The big money arrived, and it arrived fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The surprise limit reset that lit up the community
&lt;/h2&gt;

&lt;p&gt;Here is the moment that got the most love from regular users. On 2026-05-15 Anthropic manually reset everyone's rate limits, both the 5-hour window AND the weekly cap, restoring full quotas across Pro, Max, Team, and Enterprise. The official word came from the @ClaudeDevs account: "Happy Friday! We've reset everyone's 5-hour and weekly rate limits." A Friday afternoon gift, server-side, no action required.&lt;/p&gt;

&lt;p&gt;For context, this landed on top of an already generous month. Earlier in May, around the SpaceX and Colossus compute news, Anthropic had doubled the 5-hour limits across paid plans and removed the peak-hour throttling that used to quietly cut quotas during busy periods. I broke that down when it happened in my piece on &lt;a href="https://dev.to/blogs/lab/anthropic-spacex-colossus-1-300mw-220k-gpus-and-doubled-claude-limits"&gt;the May limit doubling earlier this month&lt;/a&gt;. So the reset was not a one-off apology. It was the cherry on a deliberate "use Claude more, we have the compute now" stretch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a free reset is a real signal
&lt;/h3&gt;

&lt;p&gt;A rate limit reset sounds small. It is not. Limits are the most honest signal a model provider sends about its supply. When Anthropic gives everyone a clean slate AND keeps the doubled caps, it is saying the data centers can take it. That confidence shows up in my workflow as fewer "you have hit your limit" walls in the middle of a build session.&lt;/p&gt;

&lt;p&gt;What I did with it was practical. I batched the heavy stuff. Two articles drafted, a landing page rebuilt, a Liquid section refactored, all in the window after the reset, while the quota was fresh. The lesson I keep relearning: when the platform hands you headroom, spend it on the work you have been rationing.&lt;/p&gt;

&lt;p&gt;There is a quieter signal in the timing too. The reset landed on a Friday afternoon, peak hours across the major developer regions, the exact window where you would normally expect throttling, not generosity. Handing out fresh quotas at the busiest moment is a flex. It says the compute is not the constraint anymore. For someone who used to plan a build day around limit windows, that is a genuine shift in how I work. I stopped checking the meter and started checking my own output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code w/ Claude moment that made the internet flinch
&lt;/h2&gt;

&lt;p&gt;The viral one. Across Anthropic's Code w/ Claude developer events this May, an engineer asked the room a simple question: who here has shipped a pull request in the last week that was completely written by Claude? Roughly half the hands went up. Then came the follow-up: who shipped it without reading the code? According to MIT Technology Review's 2026-05-21 coverage, most of those hands stayed up, which got a nervous laugh from the crowd.&lt;/p&gt;

&lt;p&gt;A note on logistics, because the sources differ. Simon Willison's live blog placed a San Francisco leg around 2026-05-06, while MIT Technology Review's writeup described a London stop around 2026-05-19. It was a series, not a single night, so I am not going to pin the "half the room" moment to one city. The point holds either way: in a room full of working developers, delegating a whole PR to Claude was already normal.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the coverage actually argued
&lt;/h3&gt;

&lt;p&gt;MIT Technology Review framed the event as a glimpse of "coding's future, whether you like it or not." InfoQ went deeper on the mechanics, covering Managed Agents, more proactive workflows, and a "capability curve" framing where the model keeps climbing toward harder tasks. Anthropic's own engineering lead reportedly described Claude as roughly "as good as a midlevel engineer," still needing humans for system design and the gnarly debugging.&lt;/p&gt;

&lt;p&gt;That last line is the one I underlined. Midlevel engineer for the grunt work, human for the architecture. That is not a threat to a solo builder. That is a description of how I already work. I do the deciding. Claude does the typing. The "did anyone read it" laugh is the real warning, though: shipping unread code is how you inherit bugs you cannot explain at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude for Small Business, aimed straight at people like me
&lt;/h2&gt;

&lt;p&gt;This is the launch I cared about most. On 2026-05-13 Anthropic shipped Claude for Small Business, a package of connectors plus 15 ready-to-run agentic workflows that plug Claude directly into the tools small operators already live in: Intuit QuickBooks, PayPal, HubSpot, Canva, Docusign, Google Workspace, and Microsoft 365.&lt;/p&gt;

&lt;p&gt;The workflows read like a to-do list I have been avoiding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Payroll planning&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monthly financial close and reconciliation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invoice chasing (politely getting money owed to you)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sales campaign management and lead triage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tax-season prep and contract review&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Content strategy&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Daniela Amodei, Anthropic's co-founder and president, put the pitch plainly: Claude "helps take the late-night work off their plates." For anyone who has done their own books after dinner, that line lands.&lt;/p&gt;

&lt;h3&gt;
  
  
  The extras that make it more than a feature drop
&lt;/h3&gt;

&lt;p&gt;Two add-ons stood out. First, a free "AI Fluency for Small Business" course, built with PayPal and taught by actual small-business owners, on how to use AI safely and well. Second, a "Claude SMB Tour" of free half-day workshops across 10-plus US cities starting 2026-05-14 (Chicago first, then stops including Dallas, Baltimore, San Jose, and Salt Lake City). Attendees even get a one-month Claude Max subscription thrown in.&lt;/p&gt;

&lt;p&gt;I run my whole studio solo, so the connectors that grab me are the boring ones: monthly close, invoice chasing, contract review. If you do touch the marketing side, the campaign and content workflows pair naturally with a scheduler like &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt; so the drafting and the posting both run on rails. This whole package also rhymes with &lt;a href="https://dev.to/blogs/lab/claude-just-shipped-finance-agent-templates-pitches-valuations-and-month-end-close"&gt;Claude's finance agent templates&lt;/a&gt; from earlier in the year. Anthropic is clearly building a stack for people who do not have a finance team, because they ARE the finance team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a solo studio should actually do with all this
&lt;/h2&gt;

&lt;p&gt;Roundups are useless without a move. Here is mine, in order of what paid off fastest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spend the headroom, do not hoard it
&lt;/h3&gt;

&lt;p&gt;The doubled limits and the reset mean I stopped self-rationing. I now schedule my heaviest Claude sessions in a single block and let it run: drafting, refactoring, image prompts, the lot. Treating quota as scarce when the platform is shouting "abundant" is just leaving value on the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steal the small-business workflows, skip the seats
&lt;/h3&gt;

&lt;p&gt;I do not need the enterprise contract to get the small-business brain. The 15 workflows are a template library for running a one-person operation: financial close, invoice chasing, contract review, content strategy. I picked the three that eat my evenings and wired Claude into them first. The connectors I already use (Google Workspace, plus the accounting side) did most of the heavy lifting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read the code, always
&lt;/h3&gt;

&lt;p&gt;The Code w/ Claude "did anyone read it" laugh is my permanent reminder. Half a room shipping unread AI code is a trend, not a recommendation. I let Claude write the PR. I still review every line before it ships. That single habit is the difference between leverage and liability. The cost of an unread bug is not the bug. It is the hour you spend later staring at code you never understood, in a system you did not design, while a customer waits. Reviewing the diff takes minutes. Inheriting a mystery costs a day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the validation as a sales tool
&lt;/h3&gt;

&lt;p&gt;When KPMG puts Claude in front of 276,000 people, I no longer have to argue that AI-built work is "real." The biggest, most risk-averse firms on earth already settled that for me. I lean on that when I describe how my products get made. Skeptics trust crowds.&lt;/p&gt;

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

&lt;p&gt;May 2026 was not one big Claude story. It was four smaller ones that point the same direction. Limits went up and got reset, so there is more room to build. A conference made it plain that shipping Claude-written code is already normal, with the only real caveat being "read it first." A small-business package handed solo operators the same agentic workflows the big firms are paying for. And KPMG and PwC removed the last excuse for treating AI work as not-quite-real.&lt;/p&gt;

&lt;p&gt;For a one-person studio, the net is good news. The platform I depend on is getting more generous, more capable, and more trusted, all at once. My job is to spend that headroom on real output, steal the workflows that buy back my evenings, and never ship a line I have not read.&lt;/p&gt;

&lt;p&gt;If you want the exact setup I run, the connectors, the review habits, and the guardrails that keep AI work shippable, I keep it documented in my &lt;a href="https://dev.to/pages/claude-blueprint"&gt;Claude setup blueprint&lt;/a&gt;. Take what fits your studio and leave the rest. The point is to build more, not to brag about how the work got made.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>CSS Scroll-Driven Animations: 6 Patterns I Ship in 2026</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sun, 24 May 2026 13:25:37 +0000</pubDate>
      <link>https://forem.com/raxxostudios/css-scroll-driven-animations-6-patterns-i-ship-in-2026-4bm6</link>
      <guid>https://forem.com/raxxostudios/css-scroll-driven-animations-6-patterns-i-ship-in-2026-4bm6</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A scroll progress bar needs three CSS lines with scroll(root block), zero JavaScript.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;view() plus animation-range: entry reveals cards as they enter the viewport.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Named scroll-timeline drives parallax layers at different speeds from one scroller.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sticky scrubbing ties a tall container's scroll progress to an image or text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Per-item view() timelines stagger list reveals without any offset math in JS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gate everything with @supports and kill motion under prefers-reduced-motion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deleted a 14 KB scroll library from my last project and replaced it with about 40 lines of CSS. The animations got smoother, the bundle shrank, and the main thread stopped choking on scroll listeners. Native scroll-driven animations are the reason. Here are the six patterns I actually ship in 2026, with copy-pasteable code for each.&lt;/p&gt;

&lt;p&gt;The whole feature rides on one property: &lt;code&gt;animation-timeline&lt;/code&gt;. Instead of timing an animation against a clock, you time it against scroll position. Two functions do most of the work. &lt;code&gt;scroll()&lt;/code&gt; tracks how far a scroll container has scrolled. &lt;code&gt;view()&lt;/code&gt; tracks how far an element has moved through the scrollport. Pair either with &lt;code&gt;@keyframes&lt;/code&gt; and the browser runs the animation off the main thread. No &lt;code&gt;requestAnimationFrame&lt;/code&gt;, no IntersectionObserver, no jank.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: A scroll progress bar with scroll(root block)
&lt;/h2&gt;

&lt;p&gt;The reading-progress bar at the top of a long article is the cleanest demo of the feature. It maps document scroll position straight onto a transform. No JavaScript at all.&lt;/p&gt;

&lt;p&gt;You need a fixed element and a &lt;code&gt;@keyframes&lt;/code&gt; rule that scales it from 0 to 1 on the X axis. The &lt;code&gt;scroll(root block)&lt;/code&gt; timeline reads the root scroller's progress along the block axis (vertical in most writing modes). &lt;code&gt;scaleX&lt;/code&gt; is the right transform here because it animates on the compositor and stays buttery.&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;.progress-bar&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;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;auto&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;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&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="m"&gt;#e3fc02&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform-origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&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;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grow-progress&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;grow-progress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scaleX&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Three things to notice. First, there is no &lt;code&gt;animation-duration&lt;/code&gt;. With a scroll timeline the duration is the scroll distance, so &lt;code&gt;auto&lt;/code&gt; is implied and the value you set is ignored in Chromium. Firefox is the exception: it wants a non-zero duration to apply the animation at all, so I add &lt;code&gt;animation-duration: 1ms&lt;/code&gt; defensively. Second, &lt;code&gt;linear&lt;/code&gt; matters. An eased progress bar lies about how far you have read. Third, &lt;code&gt;transform-origin: left&lt;/code&gt; anchors the growth so the bar fills left to right.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;scroll(root block)&lt;/code&gt; for page-level progress. If the scroller is a nested &lt;code&gt;overflow: auto&lt;/code&gt; panel, swap &lt;code&gt;root&lt;/code&gt; for &lt;code&gt;nearest&lt;/code&gt; and the timeline binds to the closest ancestor scroll container instead. The &lt;code&gt;block&lt;/code&gt; keyword can become &lt;code&gt;inline&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, or &lt;code&gt;y&lt;/code&gt; when you need a horizontal scroller. This pattern alone replaced a small JavaScript helper I had been copying between projects for years, part of my push toward &lt;a href="https://dev.to/blogs/lab/pure-css-animations-that-replace-javascript-libraries"&gt;pure-CSS animation patterns&lt;/a&gt; over runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Reveal-on-scroll cards with view()
&lt;/h2&gt;

&lt;p&gt;Fade-and-rise reveals used to mean an IntersectionObserver, a &lt;code&gt;.is-visible&lt;/code&gt; class, and a CSS transition. Now &lt;code&gt;view()&lt;/code&gt; does it in pure CSS. The &lt;code&gt;view()&lt;/code&gt; timeline tracks a single element as it crosses the scrollport, from the moment it enters at the bottom to the moment it leaves at the top.&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&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="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;32px&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="n"&gt;reveal&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;entry&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;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;reveal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&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="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;&lt;code&gt;animation-range: entry 0% entry 100%&lt;/code&gt; is the heart of it. The &lt;code&gt;entry&lt;/code&gt; range covers the phase where the element is entering the scrollport, from first pixel visible (&lt;code&gt;0%&lt;/code&gt;) to fully crossed the bottom edge (&lt;code&gt;100%&lt;/code&gt;). The card finishes its reveal exactly when it has entered, not when it is centered, so the motion reads as natural rather than late.&lt;/p&gt;

&lt;p&gt;I reach for &lt;code&gt;both&lt;/code&gt; as the fill mode so the start frame holds before the range begins and the end frame holds after. Without it the card snaps back to &lt;code&gt;opacity: 0&lt;/code&gt; once it scrolls past, which looks broken. Note the &lt;code&gt;translate&lt;/code&gt; property instead of a transform offset. I keep transforms free for other uses and animate &lt;code&gt;translate&lt;/code&gt; and &lt;code&gt;opacity&lt;/code&gt; only, since both are compositor-friendly.&lt;/p&gt;

&lt;p&gt;Want the reveal to start a touch later? Try &lt;code&gt;animation-range: entry 25% cover 50%&lt;/code&gt;. That delays the start until the element is a quarter into its entry and finishes it when the element covers half the scrollport. Tuning these two numbers is most of the design work. I store my preferred ranges and easings as variables so reveals stay consistent across a site, an approach I wrote up in &lt;a href="https://dev.to/blogs/lab/motion-design-tokens-that-actually-compose-durations-easings-choreography"&gt;my motion design tokens&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Parallax layers with a named scroll-timeline
&lt;/h2&gt;

&lt;p&gt;Parallax means layers moving at different speeds against the same scroll. A named scroll-timeline makes this trivial. You name the scroller once, then point as many animations at it as you like, each with its own &lt;code&gt;@keyframes&lt;/code&gt; and travel distance.&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;.parallax&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scroll-timeline-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--hero-scroll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;scroll-timeline-axis&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="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;svh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.layer-back&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="n"&gt;drift&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--hero-scroll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.layer-front&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="n"&gt;drift-fast&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--hero-scroll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;-40px&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="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;drift-fast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;-160px&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 scroller gets &lt;code&gt;scroll-timeline-name&lt;/code&gt; set to a dashed-ident (&lt;code&gt;--hero-scroll&lt;/code&gt;). Any descendant can then set &lt;code&gt;animation-timeline: --hero-scroll&lt;/code&gt; to ride that progress. Different &lt;code&gt;translate&lt;/code&gt; distances in the keyframes produce different apparent speeds, which is the parallax illusion. The background drifts 40px, the foreground 160px, across the full scroll of the container.&lt;/p&gt;

&lt;p&gt;By default a named scroll-timeline only reaches direct descendants of the scroller. If a layer lives outside that subtree, declare &lt;code&gt;timeline-scope: --hero-scroll&lt;/code&gt; on a shared ancestor and the name becomes visible across the whole tree. I only add &lt;code&gt;timeline-scope&lt;/code&gt; when the DOM forces it, because keeping the animated layers inside the scroller is simpler to reason about.&lt;/p&gt;

&lt;p&gt;One caution: parallax is the pattern most likely to feel like too much. I keep distances small and always honor reduced-motion (Pattern 6). Subtle depth beats a carnival ride every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Sticky section scrubbing
&lt;/h2&gt;

&lt;p&gt;Scrubbing is the effect where a sticky element changes as you scroll past a tall container. Think of a phone image that rotates, or a headline that swaps, driven entirely by how far through the section you have scrolled. The trick is a tall outer container with a &lt;code&gt;position: sticky&lt;/code&gt; child, animated against the container's own &lt;code&gt;view()&lt;/code&gt; progress.&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;.scrub-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="n"&gt;svh&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;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.scrub-sticky&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="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&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;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;svh&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="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;place-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.scrub-visual&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="n"&gt;scrub&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contain&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;contain&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;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;scrub&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-6deg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;scale&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="py"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0deg&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 outer &lt;code&gt;.scrub-section&lt;/code&gt; is &lt;code&gt;300svh&lt;/code&gt; tall, so it occupies three screens of scrolling. Its child sticks to the top and stays pinned while the section scrolls past. I drive &lt;code&gt;.scrub-visual&lt;/code&gt; with &lt;code&gt;animation-range: contain 0% contain 100%&lt;/code&gt;. The &lt;code&gt;contain&lt;/code&gt; range covers the entire window during which the section fully fills (or is fully contained by) the scrollport, which is exactly the pinned phase. So the visual scrubs from start to end across all three screens of stick.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;from&lt;/code&gt;/&lt;code&gt;to&lt;/code&gt; keyframes give a clean two-state scrub. Add intermediate percentages for multi-step sequences: &lt;code&gt;50% { rotate: 3deg; }&lt;/code&gt; introduces a midpoint wobble. Because the timeline is scroll-bound, scrubbing back up reverses the animation automatically. There is no state to manage and nothing to reset. This single pattern replaces the bulk of what I used to reach for a JS scroll library to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: Staggered list reveals, no JS
&lt;/h2&gt;

&lt;p&gt;Staggered reveals (items appearing one after another as the list scrolls in) usually need JavaScript to compute per-item delays. With &lt;code&gt;view()&lt;/code&gt; per item, the stagger emerges for free, because each item has its own position in the scrollport and therefore its own progress.&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;.stagger-item&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="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;24px&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="n"&gt;rise&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;rise&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;to&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="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;Each &lt;code&gt;.stagger-item&lt;/code&gt; runs the same animation against its own &lt;code&gt;view()&lt;/code&gt; timeline. Since items sit at different scroll positions, item two enters slightly after item one, so its reveal fires slightly later. The visual stagger is a side effect of layout, not of any delay value. No &lt;code&gt;nth-child&lt;/code&gt; math, no inline custom properties, no observer.&lt;/p&gt;

&lt;p&gt;The range &lt;code&gt;entry 0% cover 30%&lt;/code&gt; tunes the feel. It starts each reveal as the item begins entering and finishes it when the item covers 30% of the scrollport. A tighter range like &lt;code&gt;entry 0% entry 60%&lt;/code&gt; makes items pop faster and overlap less. A wider one spreads the motion out. For a long list, a tighter range keeps the cascade from dragging.&lt;/p&gt;

&lt;p&gt;If you want a deliberate cascade that does not depend on item spacing, you can add small &lt;code&gt;animation-delay&lt;/code&gt;-style offsets through the range itself per group. I rarely bother. The natural stagger from &lt;code&gt;view()&lt;/code&gt; looks more organic than evenly timed delays, and it adapts automatically when content reflows on mobile. One CSS rule, any number of items, correct on every viewport. This is the pattern that convinced me to delete my old reveal helper for good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 6: Reduced motion and progressive enhancement, done right
&lt;/h2&gt;

&lt;p&gt;Scroll-driven animations are an enhancement. They must never be load-bearing for content. Two guards keep them safe: a feature query so unsupported browsers get the static layout, and a reduced-motion query so people who ask for less motion get 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="c"&gt;/* Static, accessible baseline lives in the normal rules.
   Motion is added only when both checks pass. */&lt;/span&gt;
&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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;no-preference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.card&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="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;32px&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="n"&gt;reveal&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="m"&gt;0%&lt;/span&gt; &lt;span class="n"&gt;entry&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;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 structure matters. I never set &lt;code&gt;opacity: 0&lt;/code&gt; in the base rule. If I did, a browser without scroll-timeline support (or a user with reduced motion) would see permanently invisible cards. Instead the baseline is the finished, visible state. The motion, including the starting &lt;code&gt;opacity: 0&lt;/code&gt;, lives entirely inside the nested &lt;code&gt;@supports&lt;/code&gt; and &lt;code&gt;@media&lt;/code&gt; block. Strip away the enhancement and the page still reads perfectly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@supports (animation-timeline: scroll())&lt;/code&gt; gates on the actual feature, not a browser sniff. &lt;code&gt;@media (prefers-reduced-motion: no-preference)&lt;/code&gt; flips the logic so motion is opt-in by the user's OS setting, which is the polite default. If you prefer the inverse pattern, wrap the motion normally and add a &lt;code&gt;@media (prefers-reduced-motion: reduce) { animation: none; }&lt;/code&gt; override, but I find the no-preference gate harder to get wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser reality in 2026
&lt;/h3&gt;

&lt;p&gt;Here is the honest support picture as of May 2026. Chromium (Chrome and Edge) has shipped this since Chrome 115, so it has been stable for a long time. Safari shipped it in Safari 26, which closed the biggest gap. Firefox has it fully implemented but still behind a flag by default, so treat Firefox as unsupported in production and let the &lt;code&gt;@supports&lt;/code&gt; baseline carry it. caniuse puts global support around 85% and climbing. One Firefox quirk to remember: it requires a non-zero &lt;code&gt;animation-duration&lt;/code&gt; to apply scroll-driven animations, so adding &lt;code&gt;animation-duration: 1ms&lt;/code&gt; costs nothing and helps. There is also a scroll-timeline polyfill if you need the effect on older engines, though I prefer graceful degradation over shipping a polyfill.&lt;/p&gt;

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

&lt;p&gt;Native scroll-driven animations changed how I build scroll effects. Six patterns cover almost everything I need: a progress bar, reveals, parallax, sticky scrubbing, staggered lists, and the guards that keep it all safe. The total cost is a few &lt;code&gt;@keyframes&lt;/code&gt; rules and one &lt;code&gt;animation-timeline&lt;/code&gt; line each. No scroll listeners, no observers, no library to update when it breaks.&lt;/p&gt;

&lt;p&gt;The discipline that makes this work is putting the visible, accessible state in the base rules and adding motion only inside &lt;code&gt;@supports&lt;/code&gt; plus a reduced-motion check. Get that wrong and a Firefox user stares at blank cards. Get it right and the page degrades to a clean static layout everywhere, then layers in smooth, compositor-driven motion where it is welcome.&lt;/p&gt;

&lt;p&gt;If you want the durations and easings behind these snippets to compose cleanly across a whole site, that comes down to a small token system rather than one-off values. I document how I structure design and motion work, plus the rest of the studio toolkit, over at &lt;a href="https://dev.to/pages/studio"&gt;the RAXXO studio&lt;/a&gt;. Steal the patterns, tune the ranges, and delete a scroll library this week.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>CSS :has() in Production: 6 Selectors That Replaced JavaScript Across My Sites</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sat, 23 May 2026 21:39:57 +0000</pubDate>
      <link>https://forem.com/raxxostudios/css-has-in-production-6-selectors-that-replaced-javascript-across-my-sites-35f2</link>
      <guid>https://forem.com/raxxostudios/css-has-in-production-6-selectors-that-replaced-javascript-across-my-sites-35f2</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Six :has() selectors deleted roughly 240 lines of JS across my sites&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Form validation styling now uses form:has(:user-invalid), zero input listeners&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;nav:has(a[aria-current]) styles parent menus without click handlers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Theme toggle, empty-cart layout, and image cards all run on :has(), no JS&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deleted about 240 lines of JavaScript last month and the UI got faster, not worse. The trick was &lt;code&gt;:has()&lt;/code&gt;, the CSS parent selector that quietly hit Baseline in late 2023 and is boringly safe in 2026. Here are the six selectors I actually shipped to raxxo.shop and three other sites, with the JS they replaced. If you want the foundational set first, I covered those in &lt;a href="https://raxxo.shop/blogs/lab/the-css-has-patterns-that-changed-how-i-write-ui" rel="noopener noreferrer"&gt;the :has() patterns that changed how I write UI&lt;/a&gt;. This piece is the production cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form validation styling without a single input listener
&lt;/h2&gt;

&lt;p&gt;The old pattern: attach an &lt;code&gt;input&lt;/code&gt; or &lt;code&gt;blur&lt;/code&gt; listener to every field, check validity in JS, then add or remove an &lt;code&gt;is-invalid&lt;/code&gt; class on the wrapper so the label and helper text could turn red. On a six-field checkout that was a wall of event wiring, and it always desynced when a framework re-rendered the field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:has()&lt;/code&gt; plus the &lt;code&gt;:user-invalid&lt;/code&gt; pseudo-class does the whole thing in CSS. &lt;code&gt;:user-invalid&lt;/code&gt; only flips after the user has actually interacted with the field, so you do not scream red at someone who just loaded the page.&lt;/p&gt;

&lt;p&gt;Before, roughly per field:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&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="nx"&gt;field&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;blur&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;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is-invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkValidity&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;After, once, for all fields:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--field-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5470&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&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;.field__hint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff5470&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="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-valid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.field__check&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That replaced 38 lines of listener code on the checkout form alone. The border, the hint text, and a little green check now react to the input's own validity state, scoped to the wrapper so nothing leaks. I still keep one tiny bit of JS for the submit button (disabling it until the whole form is valid), because &lt;code&gt;:has()&lt;/code&gt; cannot yet style an element based on a sibling form's overall validity in a way I trust across every browser. Everything visual, though, is CSS.&lt;/p&gt;

&lt;p&gt;Two practical notes from shipping this. First, lean on the native constraint attributes (&lt;code&gt;required&lt;/code&gt;, &lt;code&gt;type="email"&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;minlength&lt;/code&gt;) so the browser does the validity math for you. &lt;code&gt;:user-invalid&lt;/code&gt; reads straight from that, which means your visual rules and your actual form submission agree by construction, with no second source of truth to drift. Second, the reduced-motion crowd gets a cleaner experience here too, since there is no class-toggle flash when a framework re-renders the field mid-typing. I measured the checkout interaction before and after on a mid-range phone: the input-to-paint delay on each keystroke dropped because the main thread no longer runs a validity check and a class write per event. It is a small number, a few milliseconds, but it is the kind of jank you feel on a long form. If you want the broader picture on dropping JS for CSS-native behavior, see &lt;a href="https://raxxo.shop/blogs/lab/pure-css-animations-that-replace-javascript-libraries" rel="noopener noreferrer"&gt;Pure CSS Animations That Replace JavaScript Libraries&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parent-of-active-child navigation that knows where you are
&lt;/h2&gt;

&lt;p&gt;Highlighting the parent menu item of the current page used to mean reading the URL in JS, looping over nav links, and toggling an &lt;code&gt;active&lt;/code&gt; class on the right `&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;`. Every router change meant re-running it. Miss an edge case and the wrong section stays lit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your links already carry &lt;code&gt;aria-current="page"&lt;/code&gt; (and they should, for screen readers), &lt;code&gt;:has()&lt;/code&gt; lets the parent style itself:&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;.nav__group&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;a&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;aria-current&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"page"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;.nav__label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--lime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.nav__group&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;a&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;aria-current&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"page"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--lime&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;No loop, no class toggling, no router hook. The accessibility attribute is the single source of truth, and the visual state follows it for free. This deleted a 22-line nav-highlighter module that I had copy-pasted into four projects, which means four fewer places to fix when the markup changes.&lt;/p&gt;

&lt;p&gt;One thing to watch with &lt;code&gt;:has()&lt;/code&gt; and specificity. The selector takes the specificity of its most specific argument, so &lt;code&gt;.nav__group:has(a[aria-current="page"])&lt;/code&gt; is heavier than a plain &lt;code&gt;.nav__group&lt;/code&gt;. If a later, simpler rule fails to override it, that is usually why. I keep these state selectors in a dedicated cascade layer so the ordering stays predictable and I am not fighting specificity by hand. Worth knowing before you sprinkle &lt;code&gt;:has()&lt;/code&gt; everywhere and wonder why one override stopped working.&lt;/p&gt;

&lt;p&gt;A second selector in the same family handles the mobile drawer. I wanted the page to lock scroll when the menu is open, and I was toggling a &lt;code&gt;body.menu-open&lt;/code&gt; class in JS. Now a hidden checkbox drives it and the body reacts to a state further down the tree is not possible (you cannot select up to &lt;code&gt;&lt;br&gt;
&lt;/code&gt; from a deep checkbox without a wrapper), so I scope it to the app shell instead:&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;.app&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#menu-toggle&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.app__scroll&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&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 drawer open and close animation, the overlay fade, and the scroll lock all run from one checkbox state. That is two selectors, zero JS, replacing what used to be a small state machine. The View Transitions work I did later leans on the same instinct of letting the platform hold state. Background on that is in &lt;a href="https://raxxo.shop/blogs/lab/view-transitions-api-5-patterns-i-use-across-raxxo-sites-in-2026" rel="noopener noreferrer"&gt;View Transitions API patterns I use across my sites&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empty states and quantity-aware layout, decided by content
&lt;/h2&gt;

&lt;p&gt;This is the selector that made me a believer. A cart, a search result list, a dashboard widget: they all need to look different when empty versus full, and they often need to react to how many children exist. The classic approach counts items in JS and sets a &lt;code&gt;data-count&lt;/code&gt; attribute or an &lt;code&gt;is-empty&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:has()&lt;/code&gt; reads the content directly. Empty state first:&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;.cart&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.cart__item&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.cart__empty&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;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.cart&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.cart__item&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="nc"&gt;.cart__items&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;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.cart&lt;/span&gt;&lt;span class="nd"&gt;:not&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.cart__item&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="nc"&gt;.cart__checkout&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;none&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 empty message shows only when there are no items, and the checkout button hides itself. No counting, no flag. When the last item is removed from the DOM, the layout flips on its own.&lt;/p&gt;

&lt;p&gt;Quantity-aware layout uses &lt;code&gt;:has()&lt;/code&gt; with quantity selectors. I wanted a results grid that switches to a single centered column when there is exactly one result, and a tighter grid past nine:&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;.results&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="nd"&gt;:only-child&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;minmax&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;36rem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.results&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="nd"&gt;:nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;10&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;gap&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second rule fires only once a tenth card exists, so dense lists get tighter spacing automatically. Pair it with &lt;code&gt;:has()&lt;/code&gt; to detect a loading skeleton and dim the surrounding toolbar, and you have an entire responsive-to-content system with no resize observers and no item counters. This replaced about 50 lines across the cart and the lab search page.&lt;/p&gt;

&lt;p&gt;The one caveat worth repeating: &lt;code&gt;:has()&lt;/code&gt; reacts to the DOM as rendered, so if your framework virtualizes a long list and only paints 20 of 400 rows, the &lt;code&gt;:nth-child&lt;/code&gt; math counts the painted rows, not the data behind them. Know your renderer. In practice this only bit me once, on a virtualized log viewer, and the fix was to drive the count selectors off a small wrapper attribute the renderer already set, then use &lt;code&gt;:has()&lt;/code&gt; for the purely visual states inside each row. The rule of thumb I landed on: use &lt;code&gt;:has()&lt;/code&gt; for "does this thing contain that thing" questions, and let your data layer answer "how many" when the list is virtualized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Theme toggles, image cards, and selected table rows
&lt;/h2&gt;

&lt;p&gt;Three smaller wins that each killed a chunk of script.&lt;/p&gt;

&lt;p&gt;Theme toggle without JS. I used to listen for a toggle, write &lt;code&gt;data-theme&lt;/code&gt; to `&lt;code&gt;, and persist it. For a no-persistence toggle (a preview switch in a settings panel), a checkbox and&lt;/code&gt;:has()` are enough:&lt;/p&gt;

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

&lt;p&gt;.preview:has(#dark-toggle:checked) {&lt;br&gt;
  --bg: #1f1f21;&lt;br&gt;
  --text: #F5F5F7;&lt;br&gt;
}&lt;/p&gt;

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

&lt;p&gt;The whole preview pane re-themes from one checkbox. For the site-wide theme I still use a few lines of JS, only because I want the choice saved to storage and applied before first paint to avoid a flash. Visual switching, though, needs no script.&lt;/p&gt;

&lt;p&gt;Card-with-image variants. A content card should lay out differently when it has a thumbnail versus when it is text only. Instead of adding a &lt;code&gt;has-image&lt;/code&gt; class server-side, the card detects its own image:&lt;/p&gt;

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

&lt;p&gt;.card:has(.card_&lt;em&gt;media) {&lt;br&gt;
  grid-template-columns: 8rem 1fr;&lt;br&gt;
}&lt;br&gt;
.card:has(.card&lt;/em&gt;&lt;em&gt;media img[src$=".svg"]) .card&lt;/em&gt;_media {&lt;br&gt;
  padding: 16px;&lt;br&gt;
}&lt;/p&gt;

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

&lt;p&gt;The layout adapts to whatever the CMS sends, and SVG logos even get extra padding so they do not sit edge to edge. No template branching.&lt;/p&gt;

&lt;p&gt;Selected table rows. Bulk-select tables usually toggle a &lt;code&gt;row-selected&lt;/code&gt; class on every checkbox change. With &lt;code&gt;:has()&lt;/code&gt;, the row styles itself and the table header can show a bulk-action bar the moment any row is checked:&lt;/p&gt;

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

&lt;p&gt;tr:has(input[type="checkbox"]:checked) {&lt;br&gt;
  background: rgba(227, 252, 2, 0.08);&lt;br&gt;
}&lt;br&gt;
.table:has(tbody input:checked) .table__bulkbar {&lt;br&gt;
  display: flex;&lt;br&gt;
}&lt;/p&gt;

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

&lt;p&gt;That deleted the last row-selection listener I had. Three selectors, three fewer scripts. None of this needs a build step, a library, or a polyfill in 2026. &lt;code&gt;:has()&lt;/code&gt; is Baseline (every modern engine shipped it by the end of 2023), so the only browsers that miss it are ones you have already dropped.&lt;/p&gt;

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

&lt;p&gt;Six selectors, four sites, roughly 240 lines of JavaScript gone. The pattern is always the same: find the place where JS is reading the DOM just to add a class, and let &lt;code&gt;:has()&lt;/code&gt; read the DOM instead. Forms react to their own validity, nav follows &lt;code&gt;aria-current&lt;/code&gt;, carts respond to their contents, and tables light up selected rows, all without listeners that desync on re-render. The UI got more robust because the state lives in one place, the markup, not in a script trying to mirror it.&lt;/p&gt;

&lt;p&gt;Start with the empty-state selector. It is the lowest risk and the easiest to feel. Then go hunting for &lt;code&gt;classList.toggle&lt;/code&gt; calls and ask whether the element could just look at its own children. Most of mine could. If you want the rest of how I keep RAXXO's frontend lean and on-brand, the work and the tools I lean on live at &lt;a href="https://raxxo.shop/pages/studio" rel="noopener noreferrer"&gt;the RAXXO Studios page&lt;/a&gt;. Steal the selectors, ship them, delete some code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>EU Reverse-Charge VAT for Solo SaaS Buyers: The Zero-Euro Math Most Devs Miss</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sat, 23 May 2026 21:38:40 +0000</pubDate>
      <link>https://forem.com/raxxostudios/eu-reverse-charge-vat-for-solo-saas-buyers-the-zero-euro-math-most-devs-miss-4d1p</link>
      <guid>https://forem.com/raxxostudios/eu-reverse-charge-vat-for-solo-saas-buyers-the-zero-euro-math-most-devs-miss-4d1p</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Reverse-charge means you self-account VAT on foreign SaaS and deduct it in the same return, so the net is zero&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A valid VAT ID on file with the vendor is what triggers reverse-charge instead of a consumer sale&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Standard taxation usually beats the small-business exemption once your stack is tool-heavy, because you can reclaim input VAT&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Four traps: missing VAT ID, treating VAT as a cost, skipping the zero return, buying tools as a consumer&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A solo studio in the EU buys most of its tools from abroad. AI models, design apps, hosting, schedulers, all billed from the US or Ireland. The first time one of those invoices shows VAT due, a lot of solo devs panic. The good news is boring: when you do it right, the VAT on those purchases costs you exactly nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Reverse-Charge Actually Is
&lt;/h2&gt;

&lt;p&gt;Reverse-charge is a rule for cross-border business-to-business services inside the EU VAT system. Normally the seller charges VAT and pays it to their own tax office. For digital services sold to a business in another EU country, that flips. The seller charges no VAT, and the buyer becomes responsible for accounting it instead. Hence the name: the charge is reversed onto you.&lt;/p&gt;

&lt;p&gt;Here is the part most people miss. "Accounting for it" does not mean writing a check. It means you record the VAT on both sides of your return at the same time. You add it as VAT you owe (because you imported a service), and you add the identical amount as input VAT you can reclaim (because you bought it for your business). The two entries cancel.&lt;/p&gt;

&lt;p&gt;So a 100 EUR SaaS subscription does not become 119 EUR. It stays 100 EUR in real money out the door. The VAT exists only as two matching numbers on a form, one positive, one negative, summing to zero.&lt;/p&gt;

&lt;p&gt;This applies to almost every foreign tool a studio touches. An AI coding assistant billed from the US, a video tool from Ireland, a cloud host in another member state. For US vendors the mechanism is technically "VAT on imported services from a non-EU supplier," but for a business buyer the bookkeeping result is the same shape: self-account, then deduct.&lt;/p&gt;

&lt;p&gt;The catch is that this only works cleanly if two things are true. You have to be a business with a valid VAT identification number, and you have to be on standard taxation rather than the small-business exemption. Get those wrong and the zero stops being zero. That is where the next two sections come in, because the setup matters more than the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The VAT ID Requirement and Why Standard Taxation Wins
&lt;/h2&gt;

&lt;p&gt;The trigger for reverse-charge is your VAT identification number. Not your local tax number, the EU-wide VAT ID (the one with the country prefix). When you enter that ID in a vendor's billing settings, their system recognizes you as a business in another member state and stops adding VAT. No ID, and most platforms treat you as a private consumer and charge their local rate, which you usually cannot reclaim. That single field is the difference between a zero-cost VAT flow and a permanent surcharge on every invoice.&lt;/p&gt;

&lt;p&gt;So step one is mundane: get a VAT ID, then paste it into every tool you pay for. Stripe, the AI vendors, the hosting bill, all of them have a tax field in account settings.&lt;/p&gt;

&lt;p&gt;Now the bigger decision. Many solo founders start on the small-business exemption, where you charge no VAT to your own customers and skip most VAT paperwork. It sounds simpler, and for a service business with almost no expenses it can be. But it has a hidden cost: under the exemption you generally cannot reclaim input VAT on what you buy. For a tool-heavy studio paying for ten or fifteen subscriptions, that reclaim is exactly the lever that makes reverse-charge net to zero.&lt;/p&gt;

&lt;p&gt;Drop the exemption and go to standard taxation, and the picture flips in your favor. You charge VAT to local customers and pass it on, you reclaim input VAT on your tools, and reverse-charge imports wash out completely. The admin is a periodic return instead of nothing, but for anyone comfortable with a spreadsheet or a bookkeeping tool it is a small price for stopping VAT from quietly eating into your tool budget. For the practical side of running those returns, see &lt;a href="https://raxxo.shop/blogs/lab/solo-studio-bookkeeping-in-90-minutes-a-month-my-stack-and-routine" rel="noopener noreferrer"&gt;Solo Studio Bookkeeping in 90 Minutes a Month&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a general pattern, not personalized advice. Thresholds and the exact paperwork vary by country, so confirm the specifics for where you are registered.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Zero Nets Out on Your Return
&lt;/h2&gt;

&lt;p&gt;Numbers make this concrete. These are illustrative round figures, not anyone's real books.&lt;/p&gt;

&lt;p&gt;Say in one quarter your studio buys foreign SaaS worth 1,000 EUR net. AI tools, a design app, hosting, a scheduler. Every vendor has your VAT ID, so every invoice arrives with no VAT added. Total cash out: 1,000 EUR.&lt;/p&gt;

&lt;p&gt;At your local standard rate (use 20 percent for the example), the reverse-charge VAT on that 1,000 EUR is 200 EUR. On the periodic VAT return you make two entries. First, you declare 200 EUR of VAT due on services received from abroad. Second, in the input VAT section you declare the same 200 EUR as reclaimable, because those services were bought for your taxable business. Line one says you owe 200. Line two says you are owed 200. Net effect on the return: zero.&lt;/p&gt;

&lt;p&gt;The form fields differ by country, but every EU VAT return has a box for VAT on cross-border services received and a box for deductible input VAT. Reverse-charge fills both with the same figure. You are not paying twice and you are not owed anything back. You are recording a transaction that, for a fully deductible business, has no cash consequence.&lt;/p&gt;

&lt;p&gt;Where it starts to matter is alongside the rest of your return. If you also charged VAT to local customers, that output VAT is real money you collect and forward. The reverse-charge entries sit beside it, cancelling each other, while the customer VAT flows through normally. The studio's actual position is set by what you sell and what you spend, never by the reverse-charge mechanic itself, which is designed to be neutral.&lt;/p&gt;

&lt;p&gt;One more practical note: keep the invoices. Even though no VAT changed hands with the vendor, you still need the document to support both the self-accounted VAT and the deduction. A tidy folder per quarter is enough. If you run sales through a platform like &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt;, export those records on the same cadence so everything reconciles in one sitting. For the wider workflow, &lt;a href="https://raxxo.shop/blogs/lab/solo-studio-invoicing-in-2026-stripe-tax-datev-export-ust-va-via-api" rel="noopener noreferrer"&gt;Solo Studio Invoicing in 2026&lt;/a&gt; walks through connecting invoicing, tax, and exports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three or Four Mistakes That Break the Zero
&lt;/h2&gt;

&lt;p&gt;Most reverse-charge problems are setup errors, not math errors. Four show up again and again.&lt;/p&gt;

&lt;p&gt;First, no VAT ID on file with the vendor. If you skipped that billing field, the platform charged you its local consumer VAT. That VAT is usually not reclaimable through reverse-charge, so it becomes a real surcharge on every renewal. Fix: add your VAT ID to every paid tool, and check that new invoices arrive with no VAT line.&lt;/p&gt;

&lt;p&gt;Second, booking the reverse-charge VAT as a real expense. Some founders see "VAT due" on the return and treat it as a cost, or pay it without recording the matching deduction. That turns a zero into an unnecessary payment. The VAT due and the input VAT are a pair. Record both, every time, or use software that posts the two legs automatically.&lt;/p&gt;

&lt;p&gt;Third, forgetting to file the return when it nets to zero. A quarter with only reverse-charge entries still nets to zero, but zero is a number you have to declare, not a reason to skip filing. A missed or late return invites penalties even though no tax was actually owed. Put the deadline in your calendar and file the zero like any other return.&lt;/p&gt;

&lt;p&gt;Fourth, mixing consumer and business purchases. Buying a tool with a personal account and a private card, then trying to deduct it on the studio's return, breaks the trail. The invoice has to be in the business name with the business VAT ID for reverse-charge and the deduction to hold up. Keep one clear payment method and one account for studio tools, and the bookkeeping stops being guesswork.&lt;/p&gt;

&lt;p&gt;None of these are hard. They are just easy to get wrong on the first invoice, before anyone has explained how the mechanism actually behaves.&lt;/p&gt;

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

&lt;p&gt;Reverse-charge VAT on foreign SaaS sounds intimidating and turns out to be neutral. Provide a valid VAT ID, sit on standard taxation rather than the small-business exemption, and self-account each import while deducting the same amount on the same return. The two entries cancel, and that 100 EUR tool stays 100 EUR.&lt;/p&gt;

&lt;p&gt;The studios that get tripped up are the ones treating it as a real cost, skipping the VAT ID field, or never filing the zero return. The studios that breeze through it set up the billing fields once and let a clean monthly routine carry the rest. For a tool-heavy operation, standard taxation plus reverse-charge is almost always the cheaper, calmer path.&lt;/p&gt;

&lt;p&gt;This is general practical experience, not formal tax advice. Rates, thresholds, and forms differ by country, so confirm the details for your registration before you rely on any of it. If you want to see how this fits into a working one-person operation, the wider setup lives at the &lt;a href="https://raxxo.shop/pages/studio" rel="noopener noreferrer"&gt;RAXXO Studios&lt;/a&gt; page, alongside the invoicing and bookkeeping pieces it connects to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Replaced ESLint + Prettier with Biome Across 16 Repos: Setup, Wins, and 2 Gotchas</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Sat, 23 May 2026 21:37:26 +0000</pubDate>
      <link>https://forem.com/raxxostudios/i-replaced-eslint-prettier-with-biome-across-16-repos-setup-wins-and-2-gotchas-4jk</link>
      <guid>https://forem.com/raxxostudios/i-replaced-eslint-prettier-with-biome-across-16-repos-setup-wins-and-2-gotchas-4jk</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Swapped ESLint plus Prettier for Biome across 16 repos, dropping from 5 dev dependencies to 1&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One shared biome.json now governs every project, no more per-repo config drift&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lint plus format fell from roughly 9 seconds to under 1 second on the biggest repo&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two real gotchas: a thin plugin ecosystem and one import-sorting rule that needed a manual pass&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For two years every RAXXO repo shipped with the same four-headed config monster: ESLint, Prettier, the plugins that glue them together, and the config files nobody wanted to touch. Last month I deleted all of it and moved 16 repos to Biome. The lint-plus-format step that used to take about 9 seconds now finishes in under a second, and the dev dependency list got a lot shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Walked Away From ESLint Plus Prettier
&lt;/h2&gt;

&lt;p&gt;The setup worked. That was never the problem. The problem was that keeping it working cost time on every single repo. A typical project carried &lt;code&gt;eslint&lt;/code&gt;, &lt;code&gt;prettier&lt;/code&gt;, &lt;code&gt;eslint-config-prettier&lt;/code&gt;, &lt;code&gt;eslint-plugin-import&lt;/code&gt;, and whatever framework plugin the stack needed. Five packages, two config files (&lt;code&gt;.eslintrc&lt;/code&gt; and &lt;code&gt;.prettierrc&lt;/code&gt;), and a &lt;code&gt;.eslintignore&lt;/code&gt; that always drifted out of sync with &lt;code&gt;.prettierignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Multiply that by 16 repos and the math gets ugly fast. When a Prettier major version landed, I had to bump it everywhere, re-test formatting on every project, and chase the inevitable conflicts where ESLint wanted one thing and Prettier wanted another. The classic fight was quotes and trailing commas. You install &lt;code&gt;eslint-config-prettier&lt;/code&gt; just to tell ESLint to stop arguing with the formatter. That package exists only because two tools are doing overlapping jobs.&lt;/p&gt;

&lt;p&gt;Speed was the other wall. On my largest repo, a full &lt;code&gt;eslint .&lt;/code&gt; pass sat around 7 seconds, and adding &lt;code&gt;prettier --check .&lt;/code&gt; on top pushed the combined gate close to 9 seconds. That is slow enough that you stop running it locally and let CI catch things, which means slower feedback and noisier pull requests.&lt;/p&gt;

&lt;p&gt;Biome collapses both jobs into one binary written in Rust. It is a formatter and a linter in the same tool, configured by a single &lt;code&gt;biome.json&lt;/code&gt;. No plugin bridge, no "turn off the rules that conflict with the formatter" dance, because there is only one tool deciding. I had been watching it mature for a while, and once it covered the rules I actually relied on, the cost of staying on the old stack stopped making sense. For the broader pattern of trading a pile of plugins for one focused tool, I went through the same exercise with build tooling in &lt;a href="https://raxxo.shop/blogs/lab/the-6-vite-plugins-that-replaced-my-webpack-config" rel="noopener noreferrer"&gt;the 6 Vite plugins that replaced my Webpack config&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating 16 Repos Without Losing a Weekend
&lt;/h2&gt;

&lt;p&gt;I did one repo by hand first to learn the shape of the work, then scripted the rest. Biome ships a migrate command that reads your existing config and translates it, which removed most of the guesswork.&lt;/p&gt;

&lt;p&gt;The per-repo flow was four steps. Install Biome as the only lint and format dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
bun add &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;--exact&lt;/span&gt; @biomejs/biome

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

&lt;/div&gt;



&lt;p&gt;Generate a starting config and pull in the old settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
bunx biome init
bunx biome migrate eslint &lt;span class="nt"&gt;--write&lt;/span&gt;
bunx biome migrate prettier &lt;span class="nt"&gt;--write&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Rip out the dead packages and their config files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
bun remove eslint prettier eslint-config-prettier eslint-plugin-import
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .eslintrc .eslintrc.json .eslintignore .prettierrc .prettierignore

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

&lt;/div&gt;



&lt;p&gt;Then run the one command that both formats and lints with fixes applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
bunx biome check &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;check&lt;/code&gt; command is the whole point. It formats, it lints, and with &lt;code&gt;--write&lt;/code&gt; it applies every safe fix in a single pass. No more chaining two tools in your scripts.&lt;/p&gt;

&lt;p&gt;To avoid copy-pasting config 16 times, I keep one shared &lt;code&gt;biome.json&lt;/code&gt; in a tiny internal package and extend it per repo. A project config is now four lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://biomejs.dev/schemas/2.0.0/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@raxxo/biome-config"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scripts/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;The shared base holds the real rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"indentStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"space"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lineWidth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"linter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"recommended"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"actions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"organizeImports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"on"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;CI got simpler too. The old job ran two steps; now it runs one:&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;bunx biome ci .&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;biome ci&lt;/code&gt; is the read-only mode for pipelines. It checks formatting and lint rules and fails on any diff, without writing files. Across all 16 repos the migration took an afternoon, most of which was reviewing diffs rather than fixing breakage. If you want the runtime context behind these commands, I switched everything to &lt;a href="https://raxxo.shop/blogs/lab/bun-1-2-replaced-node-in-every-new-raxxo-project" rel="noopener noreferrer"&gt;Bun across every new RAXXO project&lt;/a&gt; first, which is why &lt;code&gt;bunx&lt;/code&gt; shows up everywhere here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wins Were Bigger Than I Expected
&lt;/h2&gt;

&lt;p&gt;The headline is speed. On my largest repo the combined lint-and-format gate dropped from roughly 9 seconds to under 1 second. That is not a typo and it is not cherry-picked: Biome runs its checks in parallel across cores, and a single Rust binary has none of the Node startup and plugin-resolution overhead that the old chain paid on every invocation. Smaller repos that used to take 2 to 3 seconds now feel instant, low enough that the pre-commit hook stopped being something I wanted to skip.&lt;/p&gt;

&lt;p&gt;Fast feedback changes behavior. When the check is under a second, I run it constantly while writing code instead of waiting for CI to flag a stray semicolon. Pull requests got quieter because formatting and obvious lint issues are fixed before the commit ever lands.&lt;/p&gt;

&lt;p&gt;The dependency cut was the quiet win. Each repo went from five lint-and-format packages to one. Across 16 repos that is a meaningful drop in install time, lockfile churn, and the surface area I have to keep patched when a security advisory lands. Fewer transitive dependencies also means fewer of those 2am supply-chain headaches.&lt;/p&gt;

&lt;p&gt;Then there is the mental overhead I got back. One config format. One CLI to remember. One thing to upgrade when a new version ships, instead of coordinating ESLint, Prettier, and three plugins that all version independently. New repos are faster to spin up because the lint-and-format story is &lt;code&gt;extends&lt;/code&gt; one shared config and done. This is the same standardization payoff I wrote about after I unified icons in &lt;a href="https://raxxo.shop/blogs/lab/why-i-standardized-on-phosphor-icons-across-15-repos-and-cut-60-of-icon-bundle-size" rel="noopener noreferrer"&gt;why I standardized on Phosphor icons across my repos&lt;/a&gt;, where collapsing many choices into one default removed a whole category of decisions.&lt;/p&gt;

&lt;p&gt;I also stopped paying for the &lt;code&gt;eslint-config-prettier&lt;/code&gt; tax entirely. When one tool owns both formatting and linting, there is nothing to reconcile, so a whole class of conflict bugs simply does not exist anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Gotchas I Will Not Sugarcoat
&lt;/h2&gt;

&lt;p&gt;This was not free of friction, and pretending otherwise helps nobody.&lt;/p&gt;

&lt;p&gt;The first gotcha is the plugin ecosystem. ESLint has a decade of community rules, and Biome does not match that breadth yet. Most of what I relied on has a built-in Biome equivalent, but a few specialized ESLint plugins had no direct replacement. One repo leaned on a custom rule that enforced an internal import-boundary convention, and there was no Biome rule for it. I had two choices: drop the check or move it into a small standalone script. I moved it. If your team depends on a niche plugin or a hand-written ESLint rule, audit that before you commit to the switch, because you may end up rebuilding it.&lt;/p&gt;

&lt;p&gt;The second gotcha was import sorting, and it bit me on the first repo. Biome organizes imports through its assist actions, not through a lint rule, and its sort order is not identical to what &lt;code&gt;eslint-plugin-import&lt;/code&gt; produced. The first time I ran &lt;code&gt;biome check --write&lt;/code&gt; it re-sorted imports across the whole codebase, which created a large and noisy diff. Worse, in two files the new grouping reordered a side-effect import (a CSS file that had to load before a component) and the ordering actually mattered at runtime. Nothing crashed loudly, but a style sheet loaded a beat late. The fix was to run the import organize step on its own first, review that diff in isolation, then commit it separately before touching any logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
bunx biome check &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="nt"&gt;--formatter-enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nt"&gt;--linter-enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Doing the import pass as its own commit kept the real review readable and let me catch the side-effect ordering before it shipped. Once that one-time cleanup was in, every repo after it went smoothly. Lesson learned: treat the first import re-sort as a dedicated migration commit, not a side effect of your normal check.&lt;/p&gt;

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

&lt;p&gt;If you run more than a couple of JavaScript or TypeScript repos and you are tired of babysitting ESLint, Prettier, and the glue packages between them, Biome is worth a serious look. One binary, one &lt;code&gt;biome.json&lt;/code&gt;, one &lt;code&gt;biome check --write&lt;/code&gt; that formats and lints in a single pass. My combined gate went from about 9 seconds to under a second, and every repo dropped from five lint-and-format dependencies to one.&lt;/p&gt;

&lt;p&gt;It is not a drop-in for everyone. If you depend on a deep bench of ESLint plugins or custom rules, check coverage first, and plan a dedicated commit for the initial import re-sort so a stray side-effect ordering does not slip through. For most solo builders and small teams, the speed and the simpler config are well worth that one afternoon of migration.&lt;/p&gt;

&lt;p&gt;I document every one of these stack decisions as I make them, both the wins and the things that bit me. If you want to see how the rest of the toolchain fits together across the studio, the full picture lives at &lt;a href="https://raxxo.shop/pages/studio" rel="noopener noreferrer"&gt;RAXXO Studios&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>ElevenLabs Studio Workflow: 4 Patterns for 12-Minute Solo Episodes</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 22 May 2026 10:10:16 +0000</pubDate>
      <link>https://forem.com/raxxostudios/elevenlabs-studio-workflow-4-patterns-for-12-minute-solo-episodes-49he</link>
      <guid>https://forem.com/raxxostudios/elevenlabs-studio-workflow-4-patterns-for-12-minute-solo-episodes-49he</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Scripted-narration solo pod: 12 min finished in 38 min using ElevenLabs v3, costs 0.34 EUR per episode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tutorial with code intercuts uses voice cloning + SSML pauses, 51 min total, 0.52 EUR&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multi-voice debate format runs 4 voices through one v3 Studio project, 1h 12 min, 0.71 EUR&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;B-roll narration for short-form pulls 6 clips from the long script in 14 min, 0.18 EUR per pack&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I record almost zero of my own voice now. The four patterns below cover every piece of audio I ship for RAXXO Studios, from a 12-minute solo episode to a 30-second TikTok cut. Each one runs inside &lt;a href="https://try.elevenlabs.io/8pbaehnkoq4u" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt; Studio with a single project per episode. I track time-to-publish and cost per episode for each pattern because both numbers used to scare me off podcasting entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Scripted-Narration Solo Pod
&lt;/h2&gt;

&lt;p&gt;This is the workhorse. One narrator, one script, one finished 12-minute MP3. I use it for the weekly RAXXO Lab audio companion that mirrors a blog article.&lt;/p&gt;

&lt;p&gt;The flow is dead simple. I paste the script into v3 Studio, pick a single voice (a cloned one I made from 40 minutes of my own recordings), and let the engine render. The script is the blog post with three edits: contractions added, em dashes replaced with commas, and any inline code stripped out so the narrator does not try to read curly braces.&lt;/p&gt;

&lt;p&gt;What v3 Studio does well here is paragraph-level emotion tagging. I drop a &lt;code&gt;[thoughtful]&lt;/code&gt; or &lt;code&gt;[laughing slightly]&lt;/code&gt; tag at the top of paragraphs that need a tone shift. Two or three tags per 12-minute script is the right amount. More than that and the voice starts to sound theatrical.&lt;/p&gt;

&lt;p&gt;Time-to-publish from final script: 38 minutes. Breakdown: 6 minutes to paste and tag, 9 minutes to render in 1500-character chunks, 14 minutes to scrub and regenerate three lines that came out wrong, 9 minutes to export, normalize loudness in Auphonic, and upload to the host.&lt;/p&gt;

&lt;p&gt;Cost per episode: 0.34 EUR. A 12-minute script is roughly 1800 spoken words, which is 9500 characters. At the Creator plan rate of about 18 EUR for 500,000 characters, that lands at 0.34 EUR of consumed budget. Two regenerations push it to 0.36 EUR. For comparison, the same episode recorded the old way (record, edit, master) used to eat 2.5 hours of my time and roughly 70 EUR of opportunity cost.&lt;/p&gt;

&lt;p&gt;The killer feature is consistency. Episode 12 sounds exactly like episode 1. The cloned voice does not have a cold, did not stay up late, and never has a Berlin tram going past the window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Narrated Tutorial With Code Intercuts
&lt;/h2&gt;

&lt;p&gt;Tutorials need to switch register. The narration explains the why in a warm tone, the code reading needs to be slower and more precise, and the wrap-up returns to the warm tone. v3 Studio handles this with a single project plus SSML pauses.&lt;/p&gt;

&lt;p&gt;The setup: I segment the script into three voice blocks per section. Block A is the explanation, block B is the code walkthrough, block C is the takeaway. I drop a 500-millisecond SSML pause before each code block (``) and slow the code reading by 8% using the speed slider on that block only. The voice stays the same, only the pacing changes.&lt;/p&gt;

&lt;p&gt;For code with symbols I write the symbols phonetically inside the script. &lt;code&gt;const fn = (x) =&amp;gt; x * 2&lt;/code&gt; becomes &lt;code&gt;const f n equals open-paren x close-paren arrow x times two&lt;/code&gt;. Ugly but it reads correctly the first time. The first 6 tutorials I tried without this hack had at least 2 regenerations per code block. Now I get most of them in one render.&lt;/p&gt;

&lt;p&gt;Time-to-publish: 51 minutes for a 12-minute tutorial episode. The extra 13 minutes over Pattern 1 is the phonetic code rewrite and the per-block speed adjustments.&lt;/p&gt;

&lt;p&gt;Cost per episode: 0.52 EUR. Tutorials run longer in characters (more verbose explanations), so the same 12-minute target spoken minutes hits roughly 14,500 characters. The slower code blocks also push character billing slightly because the engine pads silence.&lt;/p&gt;

&lt;p&gt;Two things I learned the hard way. First, never paste real terminal output into the script. The narrator will try to read it. Replace it with a one-sentence summary. Second, the &lt;code&gt;[whisper]&lt;/code&gt; tag does not survive code blocks. Use a separate voice block with manual volume reduction instead. That cost me 9 EUR of regenerations across three tutorials before I figured it out.&lt;/p&gt;

&lt;p&gt;If you want the full story on getting voice consistency right, the deeper dive lives in the &lt;a href="https://raxxo.shop/blogs/lab/elevenlabs-vs-other-ai-voice-tools-an-honest-comparison" rel="noopener noreferrer"&gt;ElevenLabs vs Other AI Voice Tools comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Multi-Voice Debate Format
&lt;/h2&gt;

&lt;p&gt;This is the most fun pattern and the only one that draws real listener feedback. Four characters debate a take, like "should solo studios run Postgres or SQLite," and each one has a distinct voice, accent, and pace. The whole 12 minutes runs inside one v3 Studio project, no audio editor needed.&lt;/p&gt;

&lt;p&gt;Voice picks: I use one cloned voice for the host, plus three library voices chosen for clear separation. A clipped British male for the skeptic, a slow Midwest American female for the practitioner, and a fast New York male for the contrarian. The library has hundreds of options; I pick the four that sound least like each other.&lt;/p&gt;

&lt;p&gt;The script is structured as a screenplay. Each line starts with &lt;code&gt;[host]&lt;/code&gt;, &lt;code&gt;[skeptic]&lt;/code&gt;, &lt;code&gt;[practitioner]&lt;/code&gt;, or &lt;code&gt;[contrarian]&lt;/code&gt;. v3 Studio's project view lets me assign a voice to each tag globally, so I do it once at the top of the project. Reassigning later is one click.&lt;/p&gt;

&lt;p&gt;The trick that makes this not sound like a synthesized panel: I write in actual interruptions. Lines that get cut off mid-thought with a hyphen at the end. Lines that respond to the previous speaker by name. Beats of &lt;code&gt;[laughs]&lt;/code&gt; and &lt;code&gt;[skeptical]&lt;/code&gt; sprinkled in where a human would react. These tiny touches are what make the format feel produced, not algorithmic.&lt;/p&gt;

&lt;p&gt;Time-to-publish: 1 hour 12 minutes for a 12-minute episode. Most of the extra time is writing the script. The rendering itself is no slower than Pattern 1.&lt;/p&gt;

&lt;p&gt;Cost per episode: 0.71 EUR. Four voices, more emotion tags, occasional &lt;code&gt;[laughs]&lt;/code&gt; calls that count as characters. Higher than Pattern 1 but still under one euro per finished episode.&lt;/p&gt;

&lt;p&gt;For the bigger picture on where AI voice is going and the ethics around it, &lt;a href="https://raxxo.shop/blogs/lab/ai-voice-generation-tools-ethics-and-practical-use-cases" rel="noopener noreferrer"&gt;AI Voice Generation: Tools, Ethics, and Practical Use Cases&lt;/a&gt; is the primer I send people first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: B-Roll Narration for Short-Form Social Cuts
&lt;/h2&gt;

&lt;p&gt;The fourth pattern is the multiplier. Every long episode I produce gets sliced into six short-form clips for Reels, Shorts, and TikTok. Instead of re-recording the narration for each cut, I extract six 25-to-40-second segments straight from the long-form script and re-render them with v3 Studio at a slightly punchier delivery.&lt;/p&gt;

&lt;p&gt;The reason I re-render rather than slice the long-form audio: short-form needs different pacing. The long episode breathes. A Reel cannot afford a 600ms pause. So I take the same words, drop the SSML pauses, bump the speed by 5%, and add a &lt;code&gt;[confident]&lt;/code&gt; tag at the start. Same voice, tighter delivery.&lt;/p&gt;

&lt;p&gt;I batch all six into one v3 Studio project. Each segment is its own block so I can re-render any single one without touching the others. Total render time is under 3 minutes for six clips.&lt;/p&gt;

&lt;p&gt;For B-roll footage I pair the audio with motion plates generated from &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; and a few stock clips I keep in a per-episode folder. The visuals get queued separately, but the narration is locked in by the time I open the editor.&lt;/p&gt;

&lt;p&gt;Time-to-publish for a 6-clip pack: 14 minutes. Breakdown: 4 minutes to slice the script, 3 minutes to render, 4 minutes to export and rename, 3 minutes to drop into the social scheduler.&lt;/p&gt;

&lt;p&gt;Cost per pack: 0.18 EUR. Each clip averages 700 characters, so six clips is about 4200 characters. Roughly 0.15 EUR plus a regeneration or two.&lt;/p&gt;

&lt;p&gt;Distribution is handled by &lt;a href="https://join.buffer.com/raxxo-studios" rel="noopener noreferrer"&gt;Buffer&lt;/a&gt;. One scheduler holds the long-form post on Spotify, the YouTube clip on the studio channel, and the six short-form pieces across Instagram, TikTok, and YouTube Shorts. The whole pack goes from voice render to scheduled in under 30 minutes.&lt;/p&gt;

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

&lt;p&gt;Four patterns, one tool, every audio piece I ship for RAXXO Studios. Total cost for a long episode plus its six short-form children: about 0.52 EUR for the workhorse case, 0.89 EUR for the debate format. Total time-to-publish for the full package, long plus shorts: 52 to 86 minutes depending on the pattern.&lt;/p&gt;

&lt;p&gt;What used to be a podcast production stack with a microphone, a DAW, a noise reducer, a leveler, and three hours of attention is now a paste-tag-render loop inside a browser. The voice sounds like me because the cloned voice IS me, just one that does not get tired. The math works at zero scale (one episode a week) and at twenty scale (a launch week).&lt;/p&gt;

&lt;p&gt;If you want the studio storefront where all the audio companions live alongside the products they pair with, that runs on &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt;. The hub for everything in the AI Voice/Video cluster is &lt;a href="https://raxxo.shop/pages/lab-overview#ai-voice-video" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>5 agent-browser Workflows That Replaced My Manual Daily Checks</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 22 May 2026 10:01:44 +0000</pubDate>
      <link>https://forem.com/raxxostudios/5-agent-browser-workflows-that-replaced-my-manual-daily-checks-2imj</link>
      <guid>https://forem.com/raxxostudios/5-agent-browser-workflows-that-replaced-my-manual-daily-checks-2imj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Replaced 5 manual daily checks with agent-browser scripts saving 6 hours per week&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Slack unread sweep across 14 channels runs in 22 seconds, replaced a 40 EUR/month notifier&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IG insights pull every morning, killed a 29 EUR/month analytics SaaS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shopify order check and syndication status sweep run before coffee&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Competitor pricing watch fires a desktop notification when 3 rival stores move price&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used to start every morning with 12 browser tabs and a checklist. Slack, Instagram, Shopify, Dev.to, Hashnode, three competitor stores. By the time I finished checking everything, two hours were gone and the actual work had not started. So I wrote 5 &lt;a href="https://raxxo.shop/blogs/lab/ai-agents-just-got-a-real-browser" rel="noopener noreferrer"&gt;agent-browser&lt;/a&gt; scripts that do the rounds for me. Total time saved: about 6 hours per week.&lt;/p&gt;

&lt;p&gt;Here is what each one does, the actual command shape, and what it replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Slack Unread Sweep Across 14 Channels
&lt;/h2&gt;

&lt;p&gt;The first script is the one I run before I even open my laptop properly. It walks 14 Slack channels, captures any unread count above zero, and prints a single block of text I can scan in 10 seconds.&lt;/p&gt;

&lt;p&gt;Old workflow: open Slack, click every channel, mentally track which ones moved. Took about 8 minutes a day, longer if I got distracted in the first channel I opened. I had also tried a paid Slack notifier at 40 EUR per month that flooded me with desktop pings I learned to ignore.&lt;/p&gt;

&lt;p&gt;The agent-browser version connects to my already-running Chrome, navigates to each channel URL, reads the unread badge, and writes a summary to a file.&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="c1"&gt;// slack-sweep.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channels&lt;/span&gt; &lt;span class="o"&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;general&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;dev&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;design&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;sales&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;support&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for &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;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://app.slack.com/client/T0XXX/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&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;snap&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshotForAI&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;unread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; unread/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;unread&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;I run it with &lt;code&gt;dev-browser --connect ./slack-sweep.js&lt;/code&gt;. The connect flag attaches to my logged-in Chrome session, so no auth dance. Total runtime: 22 seconds for all 14 channels.&lt;/p&gt;

&lt;p&gt;The script also writes the output into my terminal statusline, so when I &lt;code&gt;cd&lt;/code&gt; into any project I see a number like "Slack: 3 channels active" without having to switch apps. That nudge replaced the panicked "did I miss something" feeling that used to drag me back into Slack three times an hour.&lt;/p&gt;

&lt;p&gt;Time saved per week: roughly 50 minutes. Money saved: 40 EUR per month from killing the paid notifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Instagram Insights Pull, Daily
&lt;/h2&gt;

&lt;p&gt;Instagram does not have a clean API for personal accounts. The Graph API needs a Business account and OAuth dance, and even then the daily insights data is two days behind. So I scrape my own insights page instead.&lt;/p&gt;

&lt;p&gt;Old workflow: open IG on phone, tap profile, tap insights, screenshot, type the numbers into a spreadsheet. Eight taps, every morning. I also paid 29 EUR per month for an analytics SaaS that mostly showed the same numbers IG already shows for free.&lt;/p&gt;

&lt;p&gt;The script opens the insights page, captures impressions and reach for the last 7 days, and appends one row to a CSV.&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="c1"&gt;// ig-insights.js&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.instagram.com/raxxostudios/insights&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForSelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[role="tab"]:has-text("Last 7 days")&lt;/span&gt;&lt;span class="dl"&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;snap&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshotForAI&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;impressions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Impressions&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;?(\d[\d&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&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;reach&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/Reach&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;?(\d[\d&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&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;today&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;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ig-daily.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;impressions&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reach&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&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;snapshotForAI()&lt;/code&gt; call is the key trick. It returns a flattened DOM string that is dramatically easier to regex than raw HTML. I learned that pattern from the dev-browser README and have leaned on it for every script since.&lt;/p&gt;

&lt;p&gt;Time saved: 10 minutes per day. SaaS cancelled: 29 EUR per month.&lt;/p&gt;

&lt;p&gt;One thing to watch: Instagram changes the DOM structure roughly every 6 weeks. My script broke twice in 3 months. Each time the fix was a 5-minute regex tweak after re-running &lt;code&gt;snapshotForAI()&lt;/code&gt; and seeing what the new labels looked like. Compared to the SaaS I cancelled (which broke for 11 days in February and went silent on support), I will gladly take a 5-minute fix every other month.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Shopify Order Check Before Coffee
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; for raxxo.shop and like to know overnight orders the moment I sit down. The admin app on phone is fine but it forces me into the phone before I have decided I am awake.&lt;/p&gt;

&lt;p&gt;Old workflow: phone in bed, unlock, app, orders, scroll. Twice I ended up doomscrolling for 30 minutes before I even stood up.&lt;/p&gt;

&lt;p&gt;The agent-browser version pulls the order count for the last 12 hours and the total sales sum, then writes it to a file I display in my terminal statusline.&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="c1"&gt;// shopify-orders.js&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://admin.shopify.com/store/c88aa1-4/orders?created_at_min=&lt;/span&gt;&lt;span class="dl"&gt;'&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;43200000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLoadState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&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;snap&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshotForAI&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; orders&lt;/span&gt;&lt;span class="se"&gt;?\s&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/shopify-status.txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Overnight: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; orders`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The Shopify admin is heavy but agent-browser handles it well because the script attaches to my real Chrome, so cookies and 2FA are already past. Runtime: 4 seconds.&lt;/p&gt;

&lt;p&gt;The bigger win is not the time, it is removing the phone from the morning. That alone was worth writing the script.&lt;/p&gt;

&lt;p&gt;I also added a second variant that pulls the inventory level for my 3 most-watched products. If any product drops below a threshold I set, it writes an entry to a &lt;code&gt;restock.txt&lt;/code&gt; file I check on Mondays. Same 10-line shape as the order check, different selector. The pattern is so reusable that any new check costs me about 10 minutes to add.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Syndication Status Sweep After Every Publish
&lt;/h2&gt;

&lt;p&gt;I cross-post every blog article to Dev.to, Hashnode, and a handful of other platforms. Most of that pipeline is API-driven, but two platforms only have web UIs, and I want to know they actually accepted the post before I move on to the next task.&lt;/p&gt;

&lt;p&gt;Old workflow: open Dev.to, search my username, scroll to find the new post, click it, confirm it rendered. Repeat on Hashnode. Repeat on Medium when I still bothered with it. Eight minutes of clicking per article.&lt;/p&gt;

&lt;p&gt;The script opens each platform's "my posts" page, checks that the most recent post matches the slug I just published, and prints a status line per platform.&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="c1"&gt;// syndication-check.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&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;targets&lt;/span&gt; &lt;span class="o"&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;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;Dev.to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://dev.to/raxxostudios&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;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;Hashnode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://raxxostudios.hashnode.dev&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;for &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;t&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;snap&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshotForAI&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LIVE&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;MISSING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;I wired this into the post-publish hook in my &lt;a href="https://raxxo.shop/blogs/lab/multi-agent-in-practice-a-5-agent-claude-pipeline-that-ships-a-blog-post-end-to-end" rel="noopener noreferrer"&gt;blog publishing pipeline&lt;/a&gt;. The minute the publish step finishes, it kicks off this check and prints to terminal. If a platform reads MISSING, I know to re-fire the syndication for that one only.&lt;/p&gt;

&lt;p&gt;Time saved per article: 8 minutes. I publish about 4 articles a week, so 32 minutes recovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Competitor Pricing Watch, Once an Hour
&lt;/h2&gt;

&lt;p&gt;Three competitor stores sell products in my space. I do not change my prices reactively, but I do want to know if all three move at once. That is usually a signal something upstream changed (supplier, ad costs, a new platform rule).&lt;/p&gt;

&lt;p&gt;Old workflow: I tried to remember to check weekly, failed most weeks, then panicked once a month and binge-checked all three.&lt;/p&gt;

&lt;p&gt;The script visits each store, captures the price of one anchor product, and appends to a log. A separate one-liner checks if all three moved in the same direction in the last 24 hours and fires a desktop notification.&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="c1"&gt;// pricing-watch.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stores&lt;/span&gt; &lt;span class="o"&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;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;rival-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://rival-a.com/products/anchor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&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;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;rival-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://rival-b.com/products/anchor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&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;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;rival-c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://rival-c.com/products/anchor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&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;for &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;s&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;snap&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshotForAI&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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/€&lt;/span&gt;&lt;span class="se"&gt;\s?(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.,&lt;/span&gt;&lt;span class="se"&gt;]?\d&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`pricing-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.log`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&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 run it on a cron job hourly with &lt;code&gt;dev-browser --headless ./pricing-watch.js&lt;/code&gt;. Headless mode is fine here because none of these stores need auth. The notification fires only when all three logs show a movement in the same direction, which has happened twice in 6 weeks.&lt;/p&gt;

&lt;p&gt;Time replaced: maybe an hour per month of manual checks. The real value is the alert, not the time.&lt;/p&gt;

&lt;p&gt;The cron line for this one looks like &lt;code&gt;0 * * * * cd /Users/me/scripts &amp;amp;&amp;amp; dev-browser --headless ./pricing-watch.js &amp;gt;&amp;gt; pricing.log 2&amp;gt;&amp;amp;1&lt;/code&gt;. Hourly is plenty, since these stores rarely move price more than once a week. I keep the logs raw and parse them with a quick awk one-liner when I want to see the 30-day trend.&lt;/p&gt;

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

&lt;p&gt;5 scripts, about 6 hours saved per week, 69 EUR per month in SaaS cancelled. The whole setup took an afternoon to write, and most of the scripts share the same 10-line pattern: connect to Chrome, navigate, snapshot, regex, write to file.&lt;/p&gt;

&lt;p&gt;If you have any morning routine that involves the same 3 browser tabs every day, you can probably replace it with a 15-line agent-browser script. The hardest part is realising you do not need a paid SaaS or a bot, you just need a script that does what you used to do with your fingers.&lt;/p&gt;

&lt;p&gt;For more on the underlying tool, see my earlier piece on &lt;a href="https://raxxo.shop/blogs/lab/ai-agents-just-got-a-real-browser" rel="noopener noreferrer"&gt;AI Agents Just Got a Real Browser&lt;/a&gt;. For other ways to chain agents together, jump to &lt;a href="https://raxxo.shop/blogs/lab/the-claude-cowork-plugin-ecosystem-6-plugins-id-install-on-day-one" rel="noopener noreferrer"&gt;The Claude Cowork Plugin Ecosystem&lt;/a&gt;. Or browse the full &lt;a href="https://raxxo.shop/pages/lab-overview" rel="noopener noreferrer"&gt;RAXXO Lab&lt;/a&gt; for more practical builds.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>5 Vercel Edge Config A/B Tests That Lifted My Shopify CTR</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 22 May 2026 10:00:26 +0000</pubDate>
      <link>https://forem.com/raxxostudios/5-vercel-edge-config-ab-tests-that-lifted-my-shopify-ctr-24gj</link>
      <guid>https://forem.com/raxxostudios/5-vercel-edge-config-ab-tests-that-lifted-my-shopify-ctr-24gj</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Hero copy split lifted clicks 18% with a 12-line Middleware snippet and zero rebuilds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Geo flag for free shipping pulled checkout intent up 9% in DE/AT/CH only&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sticky CTA variant beat the static button 14% on mobile, no layout shift&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Price display test (1.234,56€ vs 1,234.56€) won by 6% for European visitors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kill switch on a flaky review widget recovered 4% of lost add-to-carts in under 60 seconds&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ship Shopify storefronts on Vercel and use Edge Config for every A/B test now. No rebuilds, sub-15ms reads at the edge, and toggles propagate in under a second worldwide. Here are the five test patterns I actually run, with the Middleware snippets I use and the lift each one produced.&lt;/p&gt;

&lt;p&gt;If you want the full overview of why I picked this stack, see &lt;a href="https://raxxo.shop/blogs/lab/5-vercel-edge-config-patterns-i-use-for-shopify-a-b-tests" rel="noopener noreferrer"&gt;5 Vercel Edge Config Patterns I Use For Shopify A/B Tests&lt;/a&gt;. This article goes one layer deeper into the actual experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Hero Copy Split (lifted clicks 18%)
&lt;/h2&gt;

&lt;p&gt;The first test you should ever run on a &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; storefront via Edge Config is a hero copy split. Two headlines, one randomized cookie, zero rebuilds.&lt;/p&gt;

&lt;p&gt;Edge Config schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hero_copy_test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"variants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Build calmer Shopify stores."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ship Shopify stores that load in 800ms."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;Middleware (&lt;code&gt;middleware.ts&lt;/code&gt;):&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="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;get&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;@vercel/edge-config&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;NextResponse&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;next/server&lt;/span&gt;&lt;span class="dl"&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;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;test&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero_copy_test&lt;/span&gt;&lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;enabled&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero_ab&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&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;B&lt;/span&gt;&lt;span class="dl"&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hero_ab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-hero-variant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucket&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;res&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The page reads &lt;code&gt;x-hero-variant&lt;/code&gt; from the request headers and renders the right copy. I tag the variant on every PostHog event and let it run for 7 days.&lt;/p&gt;

&lt;p&gt;In my run, variant B (the specific 800ms claim) beat the calm headline by 18% on click-through to /collections/all. Outcome over vibes wins almost every time.&lt;/p&gt;

&lt;p&gt;Two notes on this pattern. First, the bucket lives in a cookie so the same visitor sees the same variant across navigations. Second, propagation time matters: when I flip &lt;code&gt;enabled&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in the dashboard, every edge POP picks it up in under a second. That is the part client-side flag libraries cannot match because they ship a JS payload and run after hydration.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Geo-Aware Free Shipping Flag (9% checkout intent)
&lt;/h2&gt;

&lt;p&gt;Free shipping converts, but global free shipping kills your shipping P&amp;amp;L. Edge Config plus the &lt;code&gt;x-vercel-ip-country&lt;/code&gt; header gives you a per-country flag with one read.&lt;/p&gt;

&lt;p&gt;Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"free_shipping_geo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"threshold_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"threshold_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"threshold_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"threshold_eur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;Middleware:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-vercel-ip-country&lt;/span&gt;&lt;span class="dl"&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;cfg&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free_shipping_geo&lt;/span&gt;&lt;span class="dl"&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;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;country&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;flag&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-free-ship&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;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-free-ship-threshold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold_eur&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 Shopify theme reads those headers via a Next.js route handler that proxies to the storefront, and the banner only renders for visitors in DE, AT, and CH. Checkout intent (proceed-to-checkout clicks) lifted 9% in those three markets while US and rest-of-world stayed flat. Edge Config let me kill the experiment for any country in one second if returns started climbing.&lt;/p&gt;

&lt;p&gt;I also covered the broader pattern in &lt;a href="https://raxxo.shop/blogs/lab/5-cloudflare-workers-patterns-i-use-for-shopify-edge-logic" rel="noopener noreferrer"&gt;5 Cloudflare Workers Patterns I Use for Shopify Edge Logic&lt;/a&gt; if you're on Cloudflare instead of Vercel.&lt;/p&gt;

&lt;p&gt;One thing to watch: &lt;code&gt;req.geo&lt;/code&gt; is populated on the Edge runtime, but the &lt;code&gt;x-vercel-ip-country&lt;/code&gt; header is the safer fallback in case you ever swap runtimes. Keep both lines. I also write the resolved country to a cookie so the page can render the banner immediately on the next navigation without re-reading headers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sticky CTA Variant (14% on mobile)
&lt;/h2&gt;

&lt;p&gt;Mobile is where the money is and where the CTA gets lost. I tested a sticky bottom-bar Add to Cart against the static in-card button.&lt;/p&gt;

&lt;p&gt;Schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sticky_cta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"split"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add to bag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"min_viewport_px"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_viewport_px"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;Middleware sets the bucket and a viewport-gated flag. The theme injects a 56px tall fixed bar that animates in after 400px of scroll. No layout shift because I reserve the height with &lt;code&gt;padding-bottom: env(safe-area-inset-bottom, 56px)&lt;/code&gt; on `` only when the variant is active.&lt;/p&gt;

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

&lt;p&gt;const sticky = await get('sticky_cta')&lt;br&gt;
if (sticky?.enabled) {&lt;br&gt;
  const bucket = Math.random() &amp;lt; sticky.split ? 'sticky' : 'static'&lt;br&gt;
  res.cookies.set('cta_ab', bucket, { maxAge: 86400 * 14 })&lt;br&gt;
  res.headers.set('x-cta-variant', bucket)&lt;br&gt;
}&lt;/p&gt;

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

&lt;p&gt;Lift: 14% more add-to-bag clicks on viewports under 768px, with no measurable hit to Largest Contentful Paint. I keep the sticky variant on for mobile and serve the static one to desktop where it already wins.&lt;/p&gt;

&lt;p&gt;The reason I gate by viewport in Edge Config (instead of CSS media queries) is so I can ramp the experiment. Day one, I set &lt;code&gt;split: 0.1&lt;/code&gt; to see 10% of mobile visitors. Day three, if the numbers look healthy, I bump it to 0.5. Day seven, I either keep it at 0.5 or roll forward. No deploy needed, just a JSON edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Price Format Experiment (6% for EU visitors)
&lt;/h2&gt;

&lt;p&gt;European number formatting matters more than people think. I tested &lt;code&gt;1.234,56€&lt;/code&gt; (EU) against &lt;code&gt;1,234.56€&lt;/code&gt; (US-with-Euro) for visitors from DE, AT, CH, NL, BE, and FR.&lt;/p&gt;

&lt;p&gt;Schema:&lt;/p&gt;

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

&lt;p&gt;{&lt;br&gt;
  "price_format": {&lt;br&gt;
    "eu_visitors": "eu",&lt;br&gt;
    "default": "us",&lt;br&gt;
    "currencies": ["EUR"]&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

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

&lt;p&gt;Middleware:&lt;/p&gt;

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

&lt;p&gt;const EU = new Set(['DE','AT','CH','NL','BE','FR'])&lt;br&gt;
const fmt = await get('price_format')&lt;br&gt;
const country = req.geo?.country || ''&lt;br&gt;
const variant = EU.has(country) ? fmt.eu_visitors : fmt.default&lt;br&gt;
res.headers.set('x-price-format', variant)&lt;/p&gt;

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

&lt;p&gt;The product page formats prices server-side from the header. The EU format won by 6% on the add-to-bag click for EU traffic. Trust signal, plain and simple. The number looks native, the brain doesn't pause, the click follows.&lt;/p&gt;

&lt;p&gt;Note: this is purely a display test. The cart and checkout always use Shopify's stored format, so accounting and tax stay clean.&lt;/p&gt;

&lt;p&gt;One trap I hit early: I forgot to pass the format through to the cart drawer, which still showed the US-style number. The mismatch confused EU buyers and one of them emailed me about it. Fix was a single prop drilling pass, but it shows why you want a single source of truth for the variant header and let every component read from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Kill Switch on a Flaky Third-Party Widget (recovered 4% add-to-carts)
&lt;/h2&gt;

&lt;p&gt;The most underrated A/B test is the one where B is "off". Third-party widgets (review apps, chat, popups) break in production. Edge Config gives you a kill switch you can flip from the dashboard without touching code.&lt;/p&gt;

&lt;p&gt;Schema:&lt;/p&gt;

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

&lt;p&gt;{&lt;br&gt;
  "widgets": {&lt;br&gt;
    "reviews": { "enabled": true, "fallback": "static" },&lt;br&gt;
    "chat":    { "enabled": true },&lt;br&gt;
    "popup":   { "enabled": false }&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 Shopify theme conditionally injects each widget's script tag based on a header set by the Middleware:&lt;/p&gt;

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

&lt;p&gt;const w = await get('widgets')&lt;br&gt;
res.headers.set('x-widgets', JSON.stringify({&lt;br&gt;
  reviews: w?.reviews?.enabled ? 1 : 0,&lt;br&gt;
  chat:    w?.chat?.enabled    ? 1 : 0,&lt;br&gt;
  popup:   w?.popup?.enabled   ? 1 : 0,&lt;br&gt;
}))&lt;/p&gt;

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

&lt;p&gt;When the review widget started failing in production (third-party CDN had a bad day), I flipped &lt;code&gt;reviews.enabled&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in Edge Config. The change hit every edge POP in under a second. The static fallback (server-rendered last 3 reviews) took over, and add-to-cart rate recovered 4% within minutes.&lt;/p&gt;

&lt;p&gt;I now treat every third-party script as an experiment with a flag. If the script breaks, the flag goes off. No deploys, no panic. Edge Config is the closest thing to a real circuit breaker on a Shopify storefront.&lt;/p&gt;

&lt;p&gt;Pair this with a tiny health-check route (&lt;code&gt;/api/health/widgets&lt;/code&gt;) that hits each third-party endpoint every few minutes and flips the flag automatically if any of them return a non-200 for 3 checks in a row. I run this on a Vercel Cron. Self-healing storefronts are not a fantasy anymore. They take maybe 50 lines of code per widget.&lt;/p&gt;

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

&lt;p&gt;Edge Config replaced a 50 EUR/month LaunchDarkly bill and a slower client-side flag library on my Shopify projects. The five patterns above run all the time on a single store and cost roughly 5 EUR/month in Vercel bandwidth.&lt;/p&gt;

&lt;p&gt;Pick one to start. Hero copy split is the lowest risk and highest learning per day. The kill switch is the highest insurance per minute. Once you have the Middleware wired, every new test is 10 lines of JSON and a deploy preview.&lt;/p&gt;

&lt;p&gt;A few things I learned the hard way running these patterns side by side. Always namespace your keys (&lt;code&gt;hero_copy_test&lt;/code&gt;, not &lt;code&gt;hero&lt;/code&gt;), so two experiments don't fight for the same field when you delete or rename. Always log the variant on every analytics event so you can slice later. And never read more than 2 keys per request in Middleware: Edge Config is fast, but every read still adds 2-5ms. Group related flags into one object.&lt;/p&gt;

&lt;p&gt;If you want the broader stack context, &lt;a href="https://raxxo.shop/blogs/lab/the-5-vercel-cron-jobs-that-keep-my-studio-running" rel="noopener noreferrer"&gt;The 5 Vercel Cron Jobs That Keep My Studio Running&lt;/a&gt; pairs nicely. Crons for the writes, Edge Config for the reads. Together they cover most of what a solo Shopify operator needs from Vercel.&lt;/p&gt;

&lt;p&gt;More patterns and write-ups in the &lt;a href="https://raxxo.shop/blogs/lab" rel="noopener noreferrer"&gt;RAXXO Lab&lt;/a&gt; if you want to keep going.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>Why I Standardized on Phosphor Icons Across 15 Repos (And Cut 60% of Icon Bundle Size)</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Fri, 22 May 2026 00:04:23 +0000</pubDate>
      <link>https://forem.com/raxxostudios/why-i-standardized-on-phosphor-icons-across-15-repos-and-cut-60-of-icon-bundle-size-fkf</link>
      <guid>https://forem.com/raxxostudios/why-i-standardized-on-phosphor-icons-across-15-repos-and-cut-60-of-icon-bundle-size-fkf</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Five icon systems across 15 repos created visual drift and 84KB bundles in the worst Next.js app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Phosphor ships six weights from one designer, so the whole studio reads as one product.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Liquid themes use Fill weight, React apps use Regular, never mixed in the same surface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A 40-icon migration across 15 repos took one afternoon with ripgrep and a small codemod.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tree-shaken Phosphor lands at 18KB, a 78% cut on the worst case and roughly 60% on average.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I caught the problem on a Tuesday. I had two RAXXO tabs open side by side. One was a Next.js dashboard, the other a Shopify product page. Both had a small rocket icon near the primary CTA. They were the same idea, the same size, the same color. They did not look related.&lt;/p&gt;

&lt;p&gt;The dashboard icon was a thin stroked Lucide rocket. The Shopify icon was a chunky Heroicons solid. Next to each other they read as two different brands. I had been telling people RAXXO was one studio, one system, one feeling. The icons were calling me a liar.&lt;/p&gt;

&lt;p&gt;That afternoon I started counting how many icon systems were actually live across the studio. The answer was five. The fix was one. This is the story of how I collapsed five into one, why Phosphor won, and the bundle math that turned a vanity migration into a measurable performance win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Icon Systems I Had Before Phosphor
&lt;/h2&gt;

&lt;p&gt;Here is what was actually shipping across the 15 repos.&lt;/p&gt;

&lt;p&gt;Heroicons was the original choice in three Next.js apps. I had used both &lt;code&gt;@heroicons/react/24/solid&lt;/code&gt; and &lt;code&gt;@heroicons/react/24/outline&lt;/code&gt; because some screens needed solid and some needed outline. That meant two icon packs imported in the same app. Tree-shaking helps, but I had a habit of importing the whole module in a few places, and the worst app was pulling 84KB of icon code into the client bundle.&lt;/p&gt;

&lt;p&gt;Lucide had crept into two newer Next.js apps because I liked the stroke weight. It tree-shook better, landing at 31KB for similar icon counts. The visual rhythm was fine inside those apps. The problem was that Lucide strokes and Heroicons solids look nothing alike.&lt;/p&gt;

&lt;p&gt;Emoji as icons was the dirty secret of three vanilla HTML landing pages. A rocket emoji next to a CTA reads fine on macOS Safari. It reads like a different font on Windows Chrome. It reads like a small typographic accident on Android. I had told myself it was fine because the pages were temporary. Several of those pages had been live for a year.&lt;/p&gt;

&lt;p&gt;Custom inline SVGs were scattered across four Shopify themes. Some were ripped from old Figma exports. Some I had drawn myself at 2 a.m. They had no consistent stroke width, no consistent corner radius, no consistent viewport size. A few were 24x24, a few were 32x32, one mystery icon was 20x20 with a 1.5px stroke that looked thinner than everything around it.&lt;/p&gt;

&lt;p&gt;Font Awesome stragglers lived in two Shopify themes I had inherited from earlier work. Loading the full Font Awesome CSS to render four icons is the kind of decision you make once and then ignore for two years. I was ignoring it.&lt;/p&gt;

&lt;p&gt;Five systems, no shared visual logic, four different stroke weights on any given Tuesday. That was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Phosphor Won: Six Weights, One Visual System
&lt;/h2&gt;

&lt;p&gt;I evaluated three replacements. Heroicons was the incumbent and lost on weight count. Lucide was a strong contender, especially for tree-shaking and React ergonomics. Phosphor (&lt;a href="https://phosphoricons.com" rel="noopener noreferrer"&gt;https://phosphoricons.com&lt;/a&gt;) won on the dimension that mattered most for a studio that ships across Liquid, React, and vanilla HTML: weight options inside one family.&lt;/p&gt;

&lt;p&gt;Phosphor ships six weights of the same icon set: Thin, Light, Regular, Bold, Fill, and Duotone. Every icon is drawn by the same hand at every weight. That means I can pick a heavier weight for one platform and a lighter weight for another and still have them read as one system.&lt;/p&gt;

&lt;p&gt;That sounds like a small thing. It is the entire reason this works.&lt;/p&gt;

&lt;p&gt;Dark themes eat thin strokes. On raxxo.shop, the background is &lt;code&gt;#1f1f21&lt;/code&gt; and the text is &lt;code&gt;#F5F5F7&lt;/code&gt;. A 1.5px stroke icon at 20px size disappears into the background noise. A filled icon at the same size has presence. The Shopify theme needed Fill weight.&lt;/p&gt;

&lt;p&gt;The React apps have lighter backgrounds in places, denser layouts, and more icons per screen. A Fill weight there would feel shouty. Regular weight, with its 1.5px stroke, sits cleanly in dense UI.&lt;/p&gt;

&lt;p&gt;With Heroicons I could pick solid or outline, two options. With Lucide I had one weight and a stroke-width prop, which is not the same as a hand-drawn weight variant. With Phosphor I had six weights drawn by the same designer, and I only needed two of them.&lt;/p&gt;

&lt;p&gt;The decision was made by Friday.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Liquid vs React Split: Fill in Themes, Regular in Apps
&lt;/h2&gt;

&lt;p&gt;The rule I locked in across the studio is simple. Liquid themes get Phosphor Fill. React and Next.js apps get Phosphor Regular. No surface mixes weights.&lt;/p&gt;

&lt;p&gt;In Shopify themes I do not import a package. I drop the SVG path data inline. That keeps the theme dependency-free and lets the icon render before any JavaScript loads. Here is the convention I use in every Liquid snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="c"&gt; Phosphor Fill weight, inline SVG, no JS dependency &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcomment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;



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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ph-fill&lt;/code&gt; and &lt;code&gt;ph-rocket-launch&lt;/code&gt; classes are mine, not Phosphor's. They give me a CSS hook for hover states and color overrides without touching the SVG attributes. I grab the raw path data from the Phosphor site and paste it once into a Liquid snippet. From then on the icon is just &lt;code&gt;{% render 'icon-rocket-launch' %}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In React apps I import from &lt;code&gt;@phosphor-icons/react&lt;/code&gt;, which is the official package. It tree-shakes correctly when you import individual components. Here is the pattern I use everywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RocketLaunch&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;@phosphor-icons/react&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CTA&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="p"&gt;(&lt;/span&gt;

      &lt;span class="nx"&gt;Launch&lt;/span&gt; &lt;span class="nx"&gt;project&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;weight="regular"&lt;/code&gt; prop is explicit, even though Regular is the default. I want the convention visible in the code so the next person (or the next me) does not casually drop a Fill icon into a React app.&lt;/p&gt;

&lt;p&gt;The Shopify (&lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;https://shopify.pxf.io/5k5rj9&lt;/a&gt;) theme repos and the Next.js app repos now share a vocabulary. A rocket means the same thing in both places. It just wears different clothes for the room.&lt;/p&gt;

&lt;p&gt;This is the same logic I used when I built the &lt;a href="https://raxxo.shop/blogs/lab/the-4-tier-dark-mode-color-system-i-use-on-every-project" rel="noopener noreferrer"&gt;4-tier dark mode color system&lt;/a&gt; for these projects. Different surfaces, shared rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration: 40 Icons, 15 Repos, One Afternoon
&lt;/h2&gt;

&lt;p&gt;The studio uses about 40 unique icons across all 15 repos. I counted by grepping for icon imports across the workspace.&lt;/p&gt;

&lt;p&gt;Step one was inventory. Ripgrep made this fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
rg &lt;span class="s2"&gt;"from '@heroicons"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
rg &lt;span class="s2"&gt;"from 'lucide-react'"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
rg &lt;span class="s2"&gt;"fa-(rocket|cart|user)"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;I dumped every match into a spreadsheet with the file path, the old icon name, and the intended Phosphor equivalent. Forty rows, sorted by repo. That spreadsheet became the migration plan.&lt;/p&gt;

&lt;p&gt;Step two was the codemod for React apps. Heroicons and Lucide have different import paths and different component names, so a single regex was not enough. I wrote a small Node script that walked each repo, parsed import statements, and rewrote them. The Heroicons &lt;code&gt;RocketLaunchIcon&lt;/code&gt; became Phosphor &lt;code&gt;RocketLaunch&lt;/code&gt;. The Lucide &lt;code&gt;Rocket&lt;/code&gt; became Phosphor &lt;code&gt;RocketLaunch&lt;/code&gt;. Naming differences were handled by a hand-written map in the script. Sixty lines of code, including the map.&lt;/p&gt;

&lt;p&gt;Step three was the Liquid themes. No codemod here. I copied 40 Phosphor Fill paths into a snippet folder, one file per icon, named like &lt;code&gt;icon-rocket-launch.liquid&lt;/code&gt;. Then I did a find-and-replace across each theme: old inline SVG out, &lt;code&gt;{% render 'icon-rocket-launch' %}&lt;/code&gt; in. This was the slowest part, maybe two hours, because I had to eyeball each old icon to figure out which Phosphor name matched it.&lt;/p&gt;

&lt;p&gt;Step four was the emoji-as-icons cleanup on the vanilla HTML pages. I replaced each emoji with the same inline SVG pattern as the Liquid themes. Same paths, same class names. One copy-paste per icon.&lt;/p&gt;

&lt;p&gt;By 6 p.m. the workspace was on one icon system. Fifteen repos, one weight per platform, zero mismatches between any two RAXXO tabs.&lt;/p&gt;

&lt;p&gt;I committed each repo separately with a message that explained what changed and why. That commit log is now my reference if I forget which icons existed before. Future me will thank present me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle Math: 84KB to 18KB on the Worst Case
&lt;/h2&gt;

&lt;p&gt;The headline number is real. The worst-case Next.js app was importing 84KB of icon code before the migration. Heroicons solid plus Heroicons outline, both pulled into client bundles, with a few lazy imports that defeated tree-shaking. Lighthouse was flagging it on slower connections.&lt;/p&gt;

&lt;p&gt;After the migration, that same app imports 18KB of Phosphor icon code. Tree-shaken, individual component imports, no barrel files. That is a 78% reduction on the worst case.&lt;/p&gt;

&lt;p&gt;The other Next.js apps were already on Lucide at 31KB. They dropped to 18KB. About a 42% reduction.&lt;/p&gt;

&lt;p&gt;Average across all the JS-bundle-relevant repos, the cut is roughly 60%. That is the number I put in the title because it is the honest studio-wide figure.&lt;/p&gt;

&lt;p&gt;The Shopify themes do not have a bundle cost in the same way. Inline SVG paths add a small amount of HTML weight per page, but no JS, no font file, no extra HTTP request. The Font Awesome removal alone saved 76KB of CSS from two themes. That weight is gone from the critical render path.&lt;/p&gt;

&lt;p&gt;A few specifics on how to actually hit 18KB. Import each Phosphor icon as a named import from the root package, not from a deep path. The package's ESM build is set up so named imports tree-shake correctly. Do not import the whole module with a wildcard. Do not import from &lt;code&gt;@phosphor-icons/react/dist/...&lt;/code&gt; directly. The clean path is the boring path.&lt;/p&gt;

&lt;p&gt;For very heavy pages with rarely used icons, dynamic import works 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@phosphor-icons/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Settings&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 use this on dashboard surfaces where a settings panel is mounted lazily. It saves another 0.4KB per icon kept out of the initial bundle. Small, but it adds up across a dense app. The same approach is described in my notes on &lt;a href="https://raxxo.shop/blogs/lab/tailwind-v4-tokens-that-actually-scale" rel="noopener noreferrer"&gt;Tailwind v4 tokens&lt;/a&gt;, where lazy imports keep the critical CSS lean.&lt;/p&gt;

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

&lt;p&gt;One icon family, two weights, fifteen repos. That is the whole system. A rocket icon in a Shopify checkout and a rocket icon in a Next.js dashboard now feel like siblings. They wear different weights because their environments are different. They come from the same hand because the studio is one studio.&lt;/p&gt;

&lt;p&gt;The bundle savings were the side effect that justified the migration to the part of my brain that needs measurable wins. Cutting 84KB to 18KB on the worst app, dropping 60% on average, removing Font Awesome from two themes. Those numbers are real and they ship to every visitor.&lt;/p&gt;

&lt;p&gt;The deeper win is the one I cannot put in a Lighthouse report. Visual consistency across surfaces is what makes a studio feel like a studio instead of a folder of side projects. Phosphor (&lt;a href="https://phosphoricons.com" rel="noopener noreferrer"&gt;https://phosphoricons.com&lt;/a&gt;) gave me one family with enough weights to handle every platform I touch. I picked two of the six weights and locked the rule in. The hard part was deciding. The migration was an afternoon.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>30 Days With the Magnific Image Pipeline: What Stuck and What Got Killed</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Thu, 21 May 2026 13:10:00 +0000</pubDate>
      <link>https://forem.com/raxxostudios/30-days-with-the-magnific-image-pipeline-what-stuck-and-what-got-killed-55g4</link>
      <guid>https://forem.com/raxxostudios/30-days-with-the-magnific-image-pipeline-what-stuck-and-what-got-killed-55g4</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Spaces replaced three separate tools in my image pipeline and cut context switching to near zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Relight turned phone-shot product photos into store-ready images without a studio setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generative Expand failed on 7 out of 10 blog OG images and got cut from the pipeline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Style Transfer drifted character likeness too far for Lexxa and got benched for brand work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The single ZIP-fingerprint fix saved 8 hours a week of manual file renaming.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last Tuesday I needed an OG image for a Lab article on AI tool pricing. Thirty seconds in Spaces, a relight pass, a Photoshop trim, a Shopify API push. Done in under four minutes. Six months ago that same OG would have eaten 90 minutes across Midjourney, Photoshop, manual upload, and a coffee break I did not need.&lt;/p&gt;

&lt;p&gt;That four-minute number is why I want to write this. 30 days into running &lt;a href="https://referral.magnific.com/mQMIvsh" rel="noopener noreferrer"&gt;Magnific&lt;/a&gt; as the spine of my image pipeline, some things stuck hard and some things got quietly killed off. This is not a feature tour. It is the honest audit of what survived the month and what did not, from a solo studio that ships blog OG images, &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;Shopify&lt;/a&gt; product shots, and AI character work every single day.&lt;/p&gt;

&lt;p&gt;Magnific used to be Freepik. They rebranded on 2026-04-28, same company, same product surfaces, new name. The affiliate code stayed the same too (mQMIvsh), so if you switched away during the rebrand confusion, your old link still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stuck: Spaces Workspace Replaced Three Tools
&lt;/h2&gt;

&lt;p&gt;Spaces is Magnific's workspace concept. One canvas, multiple generations, drag and drop, version history. Before Spaces I was running Midjourney in a Discord tab, Topaz Gigapixel for upscales, and Photoshop for compositing. Three apps, three exports, three filename conventions, three places to lose work.&lt;/p&gt;

&lt;p&gt;In 30 days I generated 247 images across 18 Spaces. The Spaces canvas held generation, upscale, relight, and fill in one view. I never opened Discord for image work once. I opened Photoshop 31 times instead of 200+, and almost always for final cleanup, not for combining tools.&lt;/p&gt;

&lt;p&gt;The concrete moment that sold me: I was building a 6-image carousel for the Statusline Builder product page. Old workflow would have been 6 Midjourney prompts, 6 Topaz upscales, 6 Photoshop crops, 6 manual uploads. In Spaces I did it as one session, one history, one ZIP export, 22 minutes start to Shopify. I closed the canvas and had nothing left to clean up because everything lived in the Space.&lt;/p&gt;

&lt;p&gt;The version history is the part nobody talks about. Every generation, upscale, and relight stays addressable inside the Space. I can pull version 4 of an image I made 18 days ago without digging through a Downloads folder of 700 files named output_final_v2_real.png. For a solo studio that ships daily, that single feature kills more chaos than any prompt improvement ever did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Stuck: Relight Made Product Photography Optional
&lt;/h2&gt;

&lt;p&gt;I do not own a softbox. I never will. I shoot product on my desk with whatever light is in the room and it always looks like that, which is the wrong kind of authentic for a Shopify store.&lt;/p&gt;

&lt;p&gt;Relight takes a flat phone photo and re-renders the lighting as if it came from a studio. Real shadows, real reflections, real direction. I ran 14 product shots through it this month. Eleven shipped to the store as-is after a Photoshop background swap. The other three needed a second pass with a different light direction, which Relight handles by dragging an arrow on the source image.&lt;/p&gt;

&lt;p&gt;The number that matters here: 14 product shots in 30 days versus 4 in the previous 30. I am shipping 3.5x more product imagery because the photography step stopped being a project. My desk is the studio now. The before/after on the Pulse Dashboard hero image is the cleanest example. Phone shot at 11pm, relit at 11:04pm, on the storefront at 11:18pm.&lt;/p&gt;

&lt;p&gt;The failure mode worth flagging: Relight does not invent geometry it cannot see. If the source photo lost a shadow side to clipping, Relight cannot recover it cleanly and will sometimes paint a flat gray patch where a real shadow should fall. Shoot the source slightly flatter than you think you need, then let Relight do the dramatic lighting. The reverse order does not work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Got Killed: Generative Expand for OG Images
&lt;/h2&gt;

&lt;p&gt;Generative Expand fills outside the original frame. In theory it is perfect for OG images, since Spaces generates at square and OG is 1200x630. Expand the square outward, get the OG ratio, ship it.&lt;/p&gt;

&lt;p&gt;In practice it failed on 7 out of 10 attempts. The expanded edges generated faces that drifted, hands that grew extra fingers, gradient halos that did not match the brand, and once a chair leg that turned into a snake. I tried it across three different prompt styles and four image categories. The success rate never crossed 30 percent.&lt;/p&gt;

&lt;p&gt;What killed it was the time math. I was spending 15 minutes per OG generating, expanding, regenerating the bad edges, then giving up and Photoshop-extending manually anyway. Photoshop's Generative Fill is built on better edge logic for this specific job. After 12 days I cut Expand from the OG flow entirely and went back to generating at the right aspect ratio from the start with a wider Spaces prompt. The 18 OGs I shipped after that change took 4 minutes each. The 8 I shipped before averaged 19. Math wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Got Killed: Style Transfer for Brand Consistency
&lt;/h2&gt;

&lt;p&gt;Style Transfer takes the look of one image and applies it to another. For brand consistency this should be the holy grail. Generate Lexxa in pose A with style X, then Style Transfer to give pose B the same style X. Consistent character, different scene, no retraining.&lt;/p&gt;

&lt;p&gt;It does not work for character likeness. Style transfers nicely. Face does not. After 22 attempts across Lexxa's blog avatar, video stills, and a podcast cover concept, the face drifted on every single one. Eye spacing changed, jawline softened, hair color shifted half a shade warmer. Each one looked like a cousin of Lexxa, not Lexxa.&lt;/p&gt;

&lt;p&gt;What kept Lexxa consistent was a different workflow entirely: locked seed plus locked prompt plus manual face reference in Photoshop. Style Transfer is fine if your subject is a building, a product, or an abstract scene. For a recurring AI character you need to ship 50 times this year, it is not the tool. I still use it for environment passes (a desk scene needing the same color grading as a previous one) but it left the character workflow on day 9 and never came back.&lt;/p&gt;

&lt;p&gt;The deeper issue is that Style Transfer optimizes for vibe, not for identity. Lighting, palette, texture, mood, all transfer beautifully. The 47 micro-decisions that make a face recognizable as one specific person do not. If your brand depends on a face people remember, build the locking workflow first and treat Style Transfer as a polish step at the end, not a consistency tool at the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One Workflow Change That Saved 8 Hours a Week
&lt;/h2&gt;

&lt;p&gt;Magnific's Spaces ZIP export is reverse-creation-order. Not paste-queue order, not alphabetical, not by tag. Reverse-creation. If you generated 30 images in a Space and export the ZIP, image 30 is first and image 1 is last. This is documented nowhere I could find.&lt;/p&gt;

&lt;p&gt;For two weeks I was manually renaming files by opening each one, checking the prompt, and matching it to my batch list. That was 90 minutes per content drop, and I do roughly three content drops a week.&lt;/p&gt;

&lt;p&gt;The fix was a transcript-fingerprint step. Before exporting I copy the Space's prompt history into a text file in creation order. After export I run a tiny script that reads the EXIF prompt from each ZIP file and matches it back to my fingerprint list. The renaming is now automatic. 90 minutes became 4. Three content drops a week. That is 8 hours and 18 minutes saved every week, which over 30 days is roughly 35 hours of pure execution time back.&lt;/p&gt;

&lt;p&gt;This is the change I would do first if I were setting up the pipeline from scratch. Everything else in Magnific is great. The ZIP export ordering is the one place where a 60-second scripting fix replaces hours of manual file work, and it is not something the docs prepare you for.&lt;/p&gt;

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

&lt;p&gt;After 30 days Magnific is the spine of my image pipeline for blog OG images, Shopify product shots, and character environments. Spaces, Relight, Upscale, and prompt-based generation all earned permanent spots. Combined cost (Premium plan at 39€/month) versus the four tools they replaced is a clean 60 percent reduction in monthly software spend with more output, not less.&lt;/p&gt;

&lt;p&gt;What Magnific is not: a one-click brand consistency tool, a generative-fill replacement for Photoshop's edge work, or a substitute for a real character locking workflow. Style Transfer is benched for characters. Generative Expand is benched for OG images. Both have their uses elsewhere and I still keep them in rotation for environment and product passes.&lt;/p&gt;

&lt;p&gt;The honest 30-day verdict is that the pipeline now ships 3.5x more imagery at 60 percent of the previous cost, with 35 hours a month freed up because of one scripting fix and one workflow consolidation. If you want the longer comparison across image tools, I covered five of them head-to-head in &lt;a href="https://raxxo.shop/blogs/lab/i-tested-5-ai-image-generators-head-to-head-only-2-shipped" rel="noopener noreferrer"&gt;this Lab piece&lt;/a&gt;. Tools that ship work survive. Tools that promise and then drift get killed off. Magnific stayed.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>claudecode</category>
      <category>automation</category>
    </item>
    <item>
      <title>View Transitions API: 5 Patterns I Use Across RAXXO Sites in 2026</title>
      <dc:creator>RAXXO Studios</dc:creator>
      <pubDate>Thu, 21 May 2026 12:45:05 +0000</pubDate>
      <link>https://forem.com/raxxostudios/view-transitions-api-5-patterns-i-use-across-raxxo-sites-in-2026-3ki6</link>
      <guid>https://forem.com/raxxostudios/view-transitions-api-5-patterns-i-use-across-raxxo-sites-in-2026-3ki6</guid>
      <description>&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Same-document transitions wrap state changes like filter chips and tab switches in a single startViewTransition call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shared-element morphs use view-transition-name to animate a product card into a detail page on Shopify themes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cross-document transitions on MPAs ship with the @view-transition rule, supported in Chrome 126+, Safari 17.4, and Firefox 131.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Theme-toggle flash gets replaced by a clip-path circle wipe expanding from the click coordinates of the toggle button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Some interactions should skip view transitions entirely, and view-transition-class is the per-element scoping primitive that helps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Last week I was watching a filter chip on the RAXXO shop snap from "All" to "Tools" and the entire product grid blinked. Same DOM, same data, same scroll position. It just looked cheap. Ten minutes later I wrapped the state update in &lt;code&gt;document.startViewTransition&lt;/code&gt; and the grid cross-faded in 200ms. Nothing else changed. The shop felt twice as expensive.&lt;/p&gt;

&lt;p&gt;That is the whole pitch for the View Transitions API. You write the state change you were already going to write, and the browser handles the in-between frames. As of May 2026, caniuse reports around 92% global support. Chrome, Edge, and Safari 17.4 all ship same-document transitions. Firefox 131 added them. Cross-document transitions need Chrome 126 or newer, which is fine because they degrade silently on older browsers.&lt;/p&gt;

&lt;p&gt;Five patterns earned a spot on every RAXXO site this year. Here is how I use them, with the code I actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Same-document transitions for state changes
&lt;/h2&gt;

&lt;p&gt;This is the gateway drug. Any time you swap visible DOM in response to a click (filter chips, tab switches, sort dropdowns, "show more" toggles), you wrap the mutation in &lt;code&gt;startViewTransition&lt;/code&gt;. The browser captures a snapshot before and after, then cross-fades between them.&lt;/p&gt;

&lt;p&gt;The catch most people miss: the callback runs synchronously inside the transition. If you await a network request inside, the snapshot freezes the loading state. Fetch first, then start the transition with the data already in hand.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setFilter&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;products&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;fetchProducts&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;renderGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;renderGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;products&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 8 lines. The progressive enhancement check on line 3 is non-optional because the API is still missing on roughly 8% of sessions. Per-transition cost on a mid-range Android device sits under 16ms in my profiling, which is one frame at 60Hz. No JS animation library can match that because the compositor is doing the work directly.&lt;/p&gt;

&lt;p&gt;A small note on accessibility. The browser honors &lt;code&gt;prefers-reduced-motion&lt;/code&gt; automatically for the default cross-fade, but if you write custom &lt;code&gt;::view-transition-*&lt;/code&gt; animations you need to gate them yourself with a media query. I forgot this on the first build and a user flagged the wipe on Pattern 4 as nauseating. Three lines of CSS fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Shared-element morphs between product cards and detail pages
&lt;/h2&gt;

&lt;p&gt;This is where you earn the premium feel. When a customer clicks a product card, the card image stretches into the hero image of the product page. No fade, no cut. The same pixels appear to fly across the viewport.&lt;/p&gt;

&lt;p&gt;You tag the source element and the destination element with the same &lt;code&gt;view-transition-name&lt;/code&gt;. Each name must be unique per page (two elements with the same name in the same snapshot will throw). I generate the name from the product ID so the source card and the destination hero stay paired.&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;.product-card&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"abc123"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;.card-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-abc123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.product-detail&lt;/span&gt; &lt;span class="nc"&gt;.hero-image&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;view-transition-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product-abc123&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;On Next.js this pairs with &lt;code&gt;router.push&lt;/code&gt; wrapped in a transition. On Shopify themes (I run the RAXXO shop on Shopify, signed up via &lt;a href="https://shopify.pxf.io/5k5rj9" rel="noopener noreferrer"&gt;shopify.pxf.io/5k5rj9&lt;/a&gt;), you use the cross-document version covered in Pattern 3. Either way, the morph runs around 250ms by default, which is the sweet spot. Faster feels glitchy, slower feels sluggish.&lt;/p&gt;

&lt;p&gt;One real gotcha. If your card image is &lt;code&gt;object-fit: cover&lt;/code&gt; and the hero is &lt;code&gt;object-fit: contain&lt;/code&gt;, the browser interpolates the box shape, not the crop. You get a brief letterbox flash. Match the &lt;code&gt;object-fit&lt;/code&gt; on both ends or accept the artifact.&lt;/p&gt;

&lt;p&gt;The other thing worth saying out loud: a morph like this costs nothing extra to ship. The image is already in cache from the card. The destination DOM was going to render anyway. You are paying for one snapshot and one compositor animation. Compare that to a JS-based FLIP animation (First Last Invert Play) which has to read layout, write transforms, and clean up listeners. I removed about 40 lines of FLIP code per page when I moved to view transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Cross-document transitions on MPAs and Shopify themes
&lt;/h2&gt;

&lt;p&gt;Same-document transitions assume you control the DOM swap. On a classic multi-page site, the browser unloads the old document and parses a new one. Until 2024, that meant no view transitions across navigations. Then Chrome 126 shipped the &lt;code&gt;@view-transition&lt;/code&gt; rule, and Safari 17.4 followed. Firefox 131 caught up in late 2025.&lt;/p&gt;

&lt;p&gt;The setup is two CSS lines on both the source page and the destination page. No JavaScript at all.&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;@view-transition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;navigation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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 it. The browser opts the navigation into a transition. Add &lt;code&gt;view-transition-name&lt;/code&gt; on the elements you want to morph and you have the same Pattern 2 effect, except it now works across full page loads on a Shopify theme, an Astro static site, or a Rails MPA. The two pages must be same-origin and both opted in.&lt;/p&gt;

&lt;p&gt;I ship this on every RAXXO Shopify theme now. Collection page to product page, blog index to blog post, cart drawer to checkout. The transitions degrade to instant navigation on older browsers, so there is no risk to the 8% who do not have support. If you already moved your tokens to a clean system (I wrote about that in &lt;a href="https://raxxo.shop/blogs/lab/tailwind-v4-tokens-that-actually-scale" rel="noopener noreferrer"&gt;tailwind-v4-tokens-that-actually-scale&lt;/a&gt;), the transitions inherit your design language for free.&lt;/p&gt;

&lt;p&gt;One sharp edge. The browser only runs the transition if the user navigates via a real link click or &lt;code&gt;history.pushState&lt;/code&gt;. A meta refresh or a &lt;code&gt;window.location.replace&lt;/code&gt; skips the API entirely. If your theme uses any of those for redirects, you will not see the morph. Swap to a real anchor or accept the cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: Theme-toggle flash elimination
&lt;/h2&gt;

&lt;p&gt;Dark mode toggles have a tell. The whole screen flashes. Even with a &lt;code&gt;prefers-color-scheme&lt;/code&gt; listener and a &lt;code&gt;color-scheme&lt;/code&gt; meta tag, the moment of repaint catches the eye. View transitions plus a &lt;code&gt;clip-path&lt;/code&gt; wipe fix this in a way that feels almost theatrical.&lt;/p&gt;

&lt;p&gt;The trick: when the user clicks the toggle, you grab the click coordinates. Then you animate a circular clip-path from radius 0 at those coordinates outward to the diagonal of the viewport. The new theme appears to ripple out from the button itself.&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;toggle&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="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--cx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--cy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--cr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;ready&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;root&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--cr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--cx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--cy&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="n"&gt;rx-wipe&lt;/span&gt; &lt;span class="m"&gt;420ms&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;rx-wipe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--cx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--cy&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;12 lines of CSS and 9 lines of JS. The animation runs on the compositor thread. I have measured it on a 2019 MacBook Air and it holds 60fps. If you want the technique without view transitions you would need a full-screen overlay element and a lot of coordination. The reason this matters is the same reason I obsess over the small things in dark UIs (&lt;a href="https://raxxo.shop/blogs/lab/8-css-properties-that-make-dark-uis-feel-premium" rel="noopener noreferrer"&gt;8-css-properties-that-make-dark-uis-feel-premium&lt;/a&gt;). The toggle is the most-clicked control on a portfolio. Make it feel like the rest of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: Knowing when NOT to use view transitions
&lt;/h2&gt;

&lt;p&gt;Three places I do not reach for the API.&lt;/p&gt;

&lt;p&gt;Continuous streams. A live log, a chat message list, a stock ticker. View transitions add a snapshot cost per change. At 10 updates per second the snapshot work piles up and the main thread chokes. Use plain CSS transitions on the new items instead.&lt;/p&gt;

&lt;p&gt;Virtualized lists. If you render only the visible rows and recycle DOM nodes on scroll, view transitions will treat node recycling as element removal plus insertion. Items appear to teleport. Tag virtualized rows with &lt;code&gt;view-transition-name: none&lt;/code&gt; to opt out.&lt;/p&gt;

&lt;p&gt;Drag interactions. The user is already controlling the position. A second animation layer fights them.&lt;/p&gt;

&lt;p&gt;For everything else, &lt;code&gt;view-transition-class&lt;/code&gt; gives you per-element scoping without naming every node uniquely. It shipped alongside the cross-document syntax and now has 92% support per caniuse.&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;.product-card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-transition-class&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;::view-transition-group&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.card&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;180ms&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 tunes every card transition to 180ms without forcing me to write a &lt;code&gt;view-transition-name&lt;/code&gt; per product. The naming requirement (one name per snapshot) is what makes lists painful. Classes solve that.&lt;/p&gt;

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

&lt;p&gt;View Transitions are the rare browser feature that pays back the time you spend learning them within a single afternoon. Same-document transitions for state changes are a one-line win on every interactive surface. Cross-document transitions are the cheapest way to make a multi-page site feel like a single-page app. The theme-toggle wipe is a 21-line flourish that no client has failed to notice.&lt;/p&gt;

&lt;p&gt;The traps are real but few. Network calls belong outside the transition callback. Unique names per snapshot, or use classes. Some interactions (streams, virtualized lists, drag) should opt out via &lt;code&gt;view-transition-name: none&lt;/code&gt;. Older browsers fall back to instant change with no broken layouts, so the feature detection in Pattern 1 is the only defensive code you actually need.&lt;/p&gt;

&lt;p&gt;I started 2026 with view transitions on three sites. They are now on every shop, every landing page, and every theme I ship. The grid no longer blinks when I switch filters. That alone was worth the afternoon.&lt;/p&gt;

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